purchasekit 0.2.1

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +31 -0
  3. data/README.md +165 -0
  4. data/Rakefile +10 -0
  5. data/app/controllers/purchase_kit/application_controller.rb +5 -0
  6. data/app/controllers/purchase_kit/pay/application_controller.rb +2 -0
  7. data/app/controllers/purchase_kit/pay/purchase/completions_controller.rb +32 -0
  8. data/app/controllers/purchase_kit/pay/purchases_controller.rb +34 -0
  9. data/app/controllers/purchase_kit/pay/webhooks_controller.rb +35 -0
  10. data/app/controllers/purchase_kit/purchase/completions_controller.rb +39 -0
  11. data/app/controllers/purchase_kit/purchases_controller.rb +37 -0
  12. data/app/controllers/purchase_kit/webhooks_controller.rb +42 -0
  13. data/app/helpers/purchase_kit/pay/paywall_helper.rb +85 -0
  14. data/app/helpers/purchase_kit/paywall_helper.rb +96 -0
  15. data/app/javascript/controllers/purchasekit/paywall_controller.js +121 -0
  16. data/app/javascript/controllers/purchasekit_pay/paywall_controller.js +112 -0
  17. data/app/javascript/purchasekit/turbo_actions.js +4 -0
  18. data/app/javascript/purchasekit_pay/turbo_actions.js +4 -0
  19. data/app/models/pay/purchasekit/charge.rb +9 -0
  20. data/app/models/pay/purchasekit/customer.rb +22 -0
  21. data/app/models/pay/purchasekit/subscription.rb +33 -0
  22. data/app/views/purchase_kit/pay/purchases/_subscription_required.html.erb +3 -0
  23. data/app/views/purchase_kit/pay/purchases/create.turbo_stream.erb +8 -0
  24. data/app/views/purchase_kit/purchases/_intent.html.erb +7 -0
  25. data/config/importmap.rb +3 -0
  26. data/config/routes.rb +7 -0
  27. data/lib/pay/purchasekit.rb +25 -0
  28. data/lib/purchasekit/api_client.rb +43 -0
  29. data/lib/purchasekit/configuration.rb +51 -0
  30. data/lib/purchasekit/engine.rb +38 -0
  31. data/lib/purchasekit/error.rb +19 -0
  32. data/lib/purchasekit/events.rb +112 -0
  33. data/lib/purchasekit/pay/configuration.rb +42 -0
  34. data/lib/purchasekit/pay/engine.rb +35 -0
  35. data/lib/purchasekit/pay/error.rb +12 -0
  36. data/lib/purchasekit/pay/version.rb +5 -0
  37. data/lib/purchasekit/pay/webhook.rb +36 -0
  38. data/lib/purchasekit/pay/webhooks/base.rb +25 -0
  39. data/lib/purchasekit/pay/webhooks/subscription_canceled.rb +11 -0
  40. data/lib/purchasekit/pay/webhooks/subscription_created.rb +39 -0
  41. data/lib/purchasekit/pay/webhooks/subscription_expired.rb +11 -0
  42. data/lib/purchasekit/pay/webhooks/subscription_updated.rb +31 -0
  43. data/lib/purchasekit/pay/webhooks.rb +11 -0
  44. data/lib/purchasekit/product/demo.rb +23 -0
  45. data/lib/purchasekit/product/remote.rb +26 -0
  46. data/lib/purchasekit/product.rb +52 -0
  47. data/lib/purchasekit/purchase/intent/demo.rb +59 -0
  48. data/lib/purchasekit/purchase/intent/remote.rb +37 -0
  49. data/lib/purchasekit/purchase/intent.rb +55 -0
  50. data/lib/purchasekit/version.rb +3 -0
  51. data/lib/purchasekit/webhook_signature.rb +60 -0
  52. data/lib/purchasekit-pay.rb +15 -0
  53. data/lib/purchasekit.rb +60 -0
  54. metadata +189 -0
@@ -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,12 @@
1
+ module PurchaseKit
2
+ module Pay
3
+ class Error < ::Pay::Error
4
+ end
5
+
6
+ class NotFoundError < Error
7
+ end
8
+
9
+ class SubscriptionRequiredError < Error
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module PurchaseKit
2
+ module Pay
3
+ VERSION = "0.1.4"
4
+ end
5
+ 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,11 @@
1
+ module PurchaseKit
2
+ module Pay
3
+ module Webhooks
4
+ class SubscriptionCanceled < Base
5
+ def call(event)
6
+ update_subscription(event, status: :canceled, ends_at: parse_time(event["ends_at"]))
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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,11 @@
1
+ module PurchaseKit
2
+ module Pay
3
+ module Webhooks
4
+ class SubscriptionExpired < Base
5
+ def call(event)
6
+ update_subscription(event, status: :expired)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
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
+ broadcast_redirect(event) if event["success_path"].present?
17
+ end
18
+
19
+ private
20
+
21
+ def broadcast_redirect(event)
22
+ customer = ::Pay::Customer.find(event["customer_id"])
23
+ Turbo::StreamsChannel.broadcast_stream_to(
24
+ dom_id(customer),
25
+ content: turbo_stream_action_tag(:redirect, url: event["success_path"])
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ 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,23 @@
1
+ module PurchaseKit
2
+ class Product
3
+ # Demo implementation of Product for local development.
4
+ #
5
+ # Reads product data from PurchaseKit.config.demo_products instead
6
+ # of making API calls. Designed for use with Xcode's StoreKit testing.
7
+ #
8
+ class Demo
9
+ class << self
10
+ def find(id)
11
+ product_data = PurchaseKit.config.demo_products[id]
12
+ raise PurchaseKit::NotFoundError, "Product not found: #{id}" unless product_data
13
+
14
+ Product.new(
15
+ id: id,
16
+ apple_product_id: product_data[:apple_product_id],
17
+ google_product_id: product_data[:google_product_id]
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module PurchaseKit
2
+ class Product
3
+ # Production implementation of Product that fetches from PurchaseKit API.
4
+ #
5
+ class Remote
6
+ class << self
7
+ def find(id)
8
+ response = ApiClient.new.get("/products/#{id}")
9
+
10
+ case response.code
11
+ when 200
12
+ Product.new(
13
+ id: response["id"],
14
+ apple_product_id: response["apple_product_id"],
15
+ google_product_id: response["google_product_id"]
16
+ )
17
+ when 404
18
+ raise PurchaseKit::NotFoundError, "Product not found: #{id}"
19
+ else
20
+ raise PurchaseKit::Error, "API error: #{response.code} #{response.message}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,52 @@
1
+ module PurchaseKit
2
+ # Represents a product configured in the PurchaseKit dashboard.
3
+ #
4
+ # Products contain the store-specific product IDs for Apple and Google.
5
+ # Display text (name, description, price) should be fetched from the
6
+ # stores at runtime or defined in your views for i18n support.
7
+ #
8
+ # Example:
9
+ # product = PurchaseKit::Product.find("prod_XXXXX")
10
+ # product.apple_product_id # => "com.example.pro.annual"
11
+ # product.google_product_id # => "pro_annual"
12
+ #
13
+ class Product
14
+ attr_reader :id, :apple_product_id, :google_product_id
15
+
16
+ def initialize(id:, apple_product_id: nil, google_product_id: nil)
17
+ @id = id
18
+ @apple_product_id = apple_product_id
19
+ @google_product_id = google_product_id
20
+ end
21
+
22
+ # Get the store-specific product ID for a platform.
23
+ #
24
+ # @param platform [Symbol] :apple or :google
25
+ # @return [String] The store product ID
26
+ #
27
+ def store_product_id(platform:)
28
+ case platform
29
+ when :apple then apple_product_id
30
+ when :google then google_product_id
31
+ else raise ArgumentError, "Unknown platform: #{platform}"
32
+ end
33
+ end
34
+
35
+ # Find a product by ID.
36
+ #
37
+ # In demo mode, reads from configured demo_products.
38
+ # In production, fetches from the PurchaseKit API.
39
+ #
40
+ # @param id [String] The product ID (e.g., "prod_XXXXX" or a demo key)
41
+ # @return [Product]
42
+ # @raise [NotFoundError] if product doesn't exist
43
+ #
44
+ def self.find(id)
45
+ if PurchaseKit.config.demo_mode?
46
+ Demo.find(id)
47
+ else
48
+ Remote.find(id)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,59 @@
1
+ require "securerandom"
2
+
3
+ module PurchaseKit
4
+ module Purchase
5
+ class Intent
6
+ # Demo implementation of Intent for local development.
7
+ #
8
+ # Stores intents in memory instead of making API calls.
9
+ # Designed for use with Xcode's StoreKit testing.
10
+ #
11
+ class Demo < Intent
12
+ attr_reader :customer_id
13
+
14
+ def initialize(id:, uuid:, product:, customer_id:, success_path:)
15
+ super(id: id, uuid: uuid, product: product, success_path: success_path)
16
+ @customer_id = customer_id
17
+ end
18
+
19
+ # URL for Xcode StoreKit testing to simulate purchase completion
20
+ def xcode_completion_url
21
+ "/purchasekit/purchase/completions/#{uuid}"
22
+ end
23
+
24
+ class << self
25
+ def find(uuid)
26
+ intent = store[uuid]
27
+ raise PurchaseKit::NotFoundError, "Intent not found: #{uuid}" unless intent
28
+ intent
29
+ end
30
+
31
+ def create(product_id:, customer_id:, success_path: nil, environment: nil)
32
+ product = Product.find(product_id)
33
+ uuid = SecureRandom.uuid
34
+ id = "intent_#{SecureRandom.hex(8)}"
35
+
36
+ intent = new(
37
+ id: id,
38
+ uuid: uuid,
39
+ product: product,
40
+ customer_id: customer_id,
41
+ success_path: success_path
42
+ )
43
+ store[uuid] = intent
44
+ intent
45
+ end
46
+
47
+ def store
48
+ @store ||= {}
49
+ end
50
+
51
+ # Clear the in-memory store (useful for tests)
52
+ def clear_store!
53
+ @store = {}
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ module PurchaseKit
2
+ module Purchase
3
+ class Intent
4
+ # Production implementation of Intent that creates via PurchaseKit API.
5
+ #
6
+ class Remote < Intent
7
+ class << self
8
+ def create(product_id:, customer_id:, success_path: nil, environment: nil)
9
+ response = ApiClient.new.post("/purchase/intents", {
10
+ product_id: product_id,
11
+ customer_id: customer_id,
12
+ success_path: success_path,
13
+ environment: environment
14
+ })
15
+
16
+ case response.code
17
+ when 201
18
+ product_data = response["product"]
19
+ product = Product.new(
20
+ id: product_data["id"],
21
+ apple_product_id: product_data["apple_product_id"],
22
+ google_product_id: product_data["google_product_id"]
23
+ )
24
+ new(id: response["id"], uuid: response["uuid"], product: product, success_path: success_path)
25
+ when 402
26
+ raise PurchaseKit::SubscriptionRequiredError, response["error"] || "Subscription required for production purchases"
27
+ when 404
28
+ raise PurchaseKit::NotFoundError, "App or product not found"
29
+ else
30
+ raise PurchaseKit::Error, "API error: #{response.code} #{response.message}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,55 @@
1
+ module PurchaseKit
2
+ module Purchase
3
+ # Represents a purchase intent - the record created before a user
4
+ # initiates an in-app purchase.
5
+ #
6
+ # The intent contains a UUID that gets passed to the store as the
7
+ # appAccountToken (Apple) or obfuscatedAccountId (Google). This allows
8
+ # PurchaseKit to correlate the store's webhook with your user.
9
+ #
10
+ # Example:
11
+ # intent = PurchaseKit::Purchase::Intent.create(
12
+ # product_id: "prod_XXXXX",
13
+ # customer_id: current_user.payment_processor.id,
14
+ # success_path: "/dashboard",
15
+ # environment: "sandbox"
16
+ # )
17
+ #
18
+ # intent.uuid # => Pass to native app for store purchase
19
+ # intent.product # => Contains store product IDs
20
+ #
21
+ class Intent
22
+ attr_reader :id, :uuid, :product, :success_path
23
+
24
+ def initialize(id:, uuid:, product:, success_path: nil)
25
+ @id = id
26
+ @uuid = uuid
27
+ @product = product
28
+ @success_path = success_path
29
+ end
30
+
31
+ # Override in subclasses if needed
32
+ def xcode_completion_url
33
+ nil
34
+ end
35
+
36
+ # Create a new purchase intent.
37
+ #
38
+ # @param product_id [String] The PurchaseKit product ID
39
+ # @param customer_id [Integer, String] Your customer/user ID (will be included in webhooks)
40
+ # @param success_path [String] Where to redirect after successful purchase
41
+ # @param environment [String] "sandbox" or "production"
42
+ # @return [Intent]
43
+ # @raise [NotFoundError] if product doesn't exist
44
+ # @raise [SubscriptionRequiredError] if PurchaseKit subscription needed for production
45
+ #
46
+ def self.create(product_id:, customer_id:, success_path: nil, environment: nil)
47
+ if PurchaseKit.config.demo_mode?
48
+ Demo.create(product_id:, customer_id:, success_path:)
49
+ else
50
+ Remote.create(product_id:, customer_id:, success_path:, environment:)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module PurchaseKit
2
+ VERSION = "0.2.1"
3
+ end
@@ -0,0 +1,60 @@
1
+ require "openssl"
2
+ require "json"
3
+
4
+ module PurchaseKit
5
+ # Verifies HMAC-SHA256 signatures on incoming webhooks.
6
+ #
7
+ # The PurchaseKit SaaS signs all webhook payloads with your webhook secret.
8
+ # This class verifies those signatures to ensure webhooks are authentic.
9
+ #
10
+ # Example:
11
+ # PurchaseKit::WebhookSignature.verify!(
12
+ # payload: request.raw_post,
13
+ # signature: request.headers["X-PurchaseKit-Signature"],
14
+ # secret: PurchaseKit.config.webhook_secret
15
+ # )
16
+ #
17
+ class WebhookSignature
18
+ attr_reader :payload, :signature, :secret
19
+
20
+ def initialize(payload:, signature:, secret:)
21
+ @payload = payload
22
+ @signature = signature
23
+ @secret = secret
24
+ end
25
+
26
+ # Verify the signature. Raises SignatureVerificationError if invalid.
27
+ def verify!
28
+ if secret.blank?
29
+ raise SignatureVerificationError, "webhook_secret must be configured"
30
+ end
31
+
32
+ if signature.blank?
33
+ raise SignatureVerificationError, "Missing signature"
34
+ end
35
+
36
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
37
+ unless ActiveSupport::SecurityUtils.secure_compare(signature, expected)
38
+ raise SignatureVerificationError, "Invalid signature"
39
+ end
40
+
41
+ true
42
+ end
43
+
44
+ # Verify the signature and return the parsed JSON payload.
45
+ def verified_payload
46
+ verify!
47
+ JSON.parse(payload, symbolize_names: true)
48
+ end
49
+
50
+ class << self
51
+ def verify!(payload:, signature:, secret:)
52
+ new(payload: payload, signature: signature, secret: secret).verify!
53
+ end
54
+
55
+ def verified_payload(payload:, signature:, secret:)
56
+ new(payload: payload, signature: signature, secret: secret).verified_payload
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,15 @@
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/product/demo"
11
+ require "purchasekit/product/remote"
12
+ require "purchasekit/purchase/intent"
13
+ require "purchasekit/purchase/intent/demo"
14
+ require "purchasekit/purchase/intent/remote"
15
+ require "pay/purchasekit"
@@ -0,0 +1,60 @@
1
+ # PurchaseKit - In-app purchase webhooks for Rails
2
+ #
3
+ # This gem handles:
4
+ # - Configuration and API client
5
+ # - Product and purchase intent management
6
+ # - Webhook signature verification
7
+ # - Event callbacks for subscription lifecycle
8
+ # - Rails engine with webhooks controller and paywall helpers
9
+ # - Pay gem integration (auto-detected when Pay is present)
10
+
11
+ require "active_support"
12
+ require "active_support/core_ext/object/blank"
13
+ require "active_support/core_ext/hash/indifferent_access"
14
+ require "active_support/notifications"
15
+ require "active_support/security_utils"
16
+
17
+ require "purchasekit/version"
18
+ require "purchasekit/configuration"
19
+ require "purchasekit/error"
20
+ require "purchasekit/events"
21
+ require "purchasekit/webhook_signature"
22
+ require "purchasekit/api_client"
23
+ require "purchasekit/product"
24
+ require "purchasekit/product/demo"
25
+ require "purchasekit/product/remote"
26
+ require "purchasekit/purchase/intent"
27
+ require "purchasekit/purchase/intent/demo"
28
+ require "purchasekit/purchase/intent/remote"
29
+ require "purchasekit/engine"
30
+
31
+ module PurchaseKit
32
+ class << self
33
+ def config
34
+ @config ||= Configuration.new
35
+ end
36
+
37
+ def configure
38
+ yield(config)
39
+ end
40
+
41
+ def reset_config!
42
+ @config = Configuration.new
43
+ end
44
+
45
+ def pay_enabled?
46
+ defined?(::Pay)
47
+ end
48
+
49
+ def queue_pay_webhook(event)
50
+ PurchaseKit::Pay::Webhook.queue(event)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Load Pay integration if Pay gem is available
56
+ if PurchaseKit.pay_enabled?
57
+ require "purchasekit/pay/webhooks"
58
+ require "purchasekit/pay/webhook"
59
+ require "pay/purchasekit"
60
+ end