paddle_rails 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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +294 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/paddle_rails/application.css +16 -0
  6. data/app/assets/stylesheets/paddle_rails/tailwind.css +1824 -0
  7. data/app/assets/tailwind/application.css +1 -0
  8. data/app/controllers/concerns/paddle_rails/paddle_checkout_error_handler.rb +89 -0
  9. data/app/controllers/concerns/paddle_rails/subscription_owner.rb +16 -0
  10. data/app/controllers/paddle_rails/application_controller.rb +21 -0
  11. data/app/controllers/paddle_rails/checkout_controller.rb +121 -0
  12. data/app/controllers/paddle_rails/dashboard_controller.rb +37 -0
  13. data/app/controllers/paddle_rails/onboarding_controller.rb +55 -0
  14. data/app/controllers/paddle_rails/payments_controller.rb +62 -0
  15. data/app/controllers/paddle_rails/subscriptions_controller.rb +92 -0
  16. data/app/controllers/paddle_rails/webhooks_controller.rb +78 -0
  17. data/app/helpers/paddle_rails/application_helper.rb +121 -0
  18. data/app/helpers/paddle_rails/subscription_owner_helper.rb +14 -0
  19. data/app/jobs/paddle_rails/application_job.rb +4 -0
  20. data/app/jobs/paddle_rails/process_webhook_job.rb +38 -0
  21. data/app/mailers/paddle_rails/application_mailer.rb +6 -0
  22. data/app/models/concerns/paddle_rails/subscribable.rb +46 -0
  23. data/app/models/paddle_rails/application_record.rb +5 -0
  24. data/app/models/paddle_rails/payment.rb +43 -0
  25. data/app/models/paddle_rails/price.rb +25 -0
  26. data/app/models/paddle_rails/product.rb +16 -0
  27. data/app/models/paddle_rails/subscription.rb +87 -0
  28. data/app/models/paddle_rails/subscription_item.rb +35 -0
  29. data/app/models/paddle_rails/webhook_event.rb +51 -0
  30. data/app/presenters/paddle_rails/payment_presenter.rb +96 -0
  31. data/app/presenters/paddle_rails/product_presenter.rb +178 -0
  32. data/app/presenters/paddle_rails/subscription_presenter.rb +145 -0
  33. data/app/views/layouts/paddle_rails/application.html.erb +170 -0
  34. data/app/views/paddle_rails/checkout/show.html.erb +128 -0
  35. data/app/views/paddle_rails/dashboard/_change_plan.html.erb +286 -0
  36. data/app/views/paddle_rails/dashboard/_current_subscription.html.erb +66 -0
  37. data/app/views/paddle_rails/dashboard/_payment_history.html.erb +79 -0
  38. data/app/views/paddle_rails/dashboard/_payment_method.html.erb +48 -0
  39. data/app/views/paddle_rails/dashboard/show.html.erb +47 -0
  40. data/app/views/paddle_rails/onboarding/show.html.erb +100 -0
  41. data/app/views/paddle_rails/shared/configuration_error.html.erb +94 -0
  42. data/config/routes.rb +13 -0
  43. data/db/migrate/20251124180624_create_paddle_rails_subscription_plans.rb +18 -0
  44. data/db/migrate/20251124180817_create_paddle_rails_subscription_prices.rb +26 -0
  45. data/db/migrate/20251127221947_create_paddle_rails_webhook_events.rb +19 -0
  46. data/db/migrate/20251128135831_create_paddle_rails_subscriptions.rb +21 -0
  47. data/db/migrate/20251128142327_create_paddle_rails_subscription_items.rb +16 -0
  48. data/db/migrate/20251128151334_remove_paddle_price_id_from_subscriptions.rb +7 -0
  49. data/db/migrate/20251128151401_rename_subscription_plans_to_products.rb +6 -0
  50. data/db/migrate/20251128151402_rename_subscription_plan_id_to_subscription_product_id.rb +13 -0
  51. data/db/migrate/20251128151453_remove_subscription_price_id_from_subscriptions.rb +8 -0
  52. data/db/migrate/20251128151501_add_subscription_product_id_to_subscription_items.rb +8 -0
  53. data/db/migrate/20251128152025_remove_paddle_item_id_from_subscription_items.rb +6 -0
  54. data/db/migrate/20251128212046_rename_subscription_products_to_products.rb +6 -0
  55. data/db/migrate/20251128212047_rename_subscription_prices_to_prices.rb +6 -0
  56. data/db/migrate/20251128212053_rename_subscription_product_id_to_product_id_in_prices.rb +13 -0
  57. data/db/migrate/20251128212054_rename_fks_in_subscription_items.rb +20 -0
  58. data/db/migrate/20251128220016_add_scheduled_cancelation_at_to_subscriptions.rb +6 -0
  59. data/db/migrate/20251129121336_add_payment_method_to_subscriptions.rb +10 -0
  60. data/db/migrate/20251129222345_create_paddle_rails_payments.rb +24 -0
  61. data/lib/paddle_rails/checkout.rb +181 -0
  62. data/lib/paddle_rails/configuration.rb +121 -0
  63. data/lib/paddle_rails/engine.rb +49 -0
  64. data/lib/paddle_rails/product_sync.rb +176 -0
  65. data/lib/paddle_rails/subscription_sync.rb +303 -0
  66. data/lib/paddle_rails/version.rb +6 -0
  67. data/lib/paddle_rails/webhook_processor.rb +102 -0
  68. data/lib/paddle_rails/webhook_verifier.rb +110 -0
  69. data/lib/paddle_rails.rb +32 -0
  70. data/lib/tasks/paddle_rails_tasks.rake +15 -0
  71. metadata +157 -0
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaddleRails
4
+ # Service class for processing webhook events.
5
+ #
6
+ # Delegates to specific handlers based on event type and emits
7
+ # ActiveSupport::Notifications for host applications to listen to.
8
+ #
9
+ # @example
10
+ # PaddleRails::WebhookProcessor.process(webhook_event)
11
+ class WebhookProcessor
12
+ # Process a webhook event.
13
+ #
14
+ # @param event [PaddleRails::WebhookEvent] The webhook event to process
15
+ # @return [void]
16
+ def self.process(event)
17
+ new(event).process
18
+ end
19
+
20
+ # @param event [PaddleRails::WebhookEvent]
21
+ def initialize(event)
22
+ @event = event
23
+ @payload = event.payload
24
+ @event_type = event.event_type
25
+ end
26
+
27
+ # Process the webhook event.
28
+ #
29
+ # Emits an ActiveSupport::Notification with the event type and payload
30
+ # so host applications can subscribe to specific events.
31
+ #
32
+ # @return [void]
33
+ def process
34
+ # Emit notification for host applications to listen to
35
+ # Format: "paddle_rails.{event_type}"
36
+ notification_name = "paddle_rails.#{@event_type}"
37
+
38
+ ActiveSupport::Notifications.instrument(notification_name) do |payload|
39
+ payload[:webhook_event] = @event
40
+ payload[:event_type] = @event_type
41
+ payload[:raw_payload] = @payload
42
+
43
+ # Delegate to specific handler if it exists
44
+ handler_method = handler_method_name
45
+ if respond_to?(handler_method, true)
46
+ send(handler_method)
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Get the handler method name for the event type.
54
+ #
55
+ # Converts "subscription.created" to "handle_subscription_created"
56
+ #
57
+ # @return [String] The handler method name
58
+ def handler_method_name
59
+ "handle_#{@event_type.tr('.', '_')}"
60
+ end
61
+
62
+ # Handler for subscription.created events.
63
+ def handle_subscription_created
64
+ subscription_data = @payload["data"]
65
+ return unless subscription_data
66
+
67
+ SubscriptionSync.sync_from_payload(subscription_data)
68
+ end
69
+
70
+ # Handler for subscription.updated events.
71
+ def handle_subscription_updated
72
+ subscription_data = @payload["data"]
73
+ return unless subscription_data
74
+
75
+ SubscriptionSync.sync_from_payload(subscription_data)
76
+ end
77
+
78
+ # Handler for subscription.canceled events.
79
+ def handle_subscription_canceled
80
+ subscription_data = @payload["data"]
81
+ return unless subscription_data
82
+
83
+ SubscriptionSync.sync_from_payload(subscription_data)
84
+ end
85
+
86
+ # Handler for transaction.completed events.
87
+ #
88
+ # Syncs payment record and payment method details from the completed transaction
89
+ # to the associated subscription.
90
+ def handle_transaction_completed
91
+ transaction_data = @payload["data"]
92
+ return unless transaction_data
93
+
94
+ # Sync payment record
95
+ SubscriptionSync.sync_payment(transaction_data)
96
+
97
+ # Sync payment method from the transaction
98
+ SubscriptionSync.sync_payment_method_from_transaction(transaction_data)
99
+ end
100
+ end
101
+ end
102
+
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module PaddleRails
6
+ # Service class for verifying Paddle webhook signatures.
7
+ #
8
+ # Implements Paddle's webhook signature verification as documented at:
9
+ # https://developer.paddle.com/webhooks/signature-verification
10
+ #
11
+ # @example
12
+ # verifier = PaddleRails::WebhookVerifier.new(secret_key)
13
+ # if verifier.verify(raw_body, signature_header)
14
+ # # Webhook is valid
15
+ # end
16
+ class WebhookVerifier
17
+ # Default tolerance for timestamp validation (5 seconds)
18
+ TIMESTAMP_TOLERANCE = 5
19
+
20
+ # @param secret_key [String] The webhook secret key from Paddle
21
+ # @param timestamp_tolerance [Integer] Maximum age of webhook in seconds (default: 5)
22
+ def initialize(secret_key, timestamp_tolerance: TIMESTAMP_TOLERANCE)
23
+ @secret_key = secret_key
24
+ @timestamp_tolerance = timestamp_tolerance
25
+ end
26
+
27
+ # Verify a webhook signature.
28
+ #
29
+ # @param raw_body [String] The raw request body (must not be transformed)
30
+ # @param signature_header [String] The value of the Paddle-Signature header
31
+ # @return [Boolean] true if signature is valid, false otherwise
32
+ def verify(raw_body, signature_header)
33
+ return false if raw_body.nil? || signature_header.nil? || @secret_key.nil?
34
+
35
+ timestamp, signatures = parse_signature_header(signature_header)
36
+ return false unless timestamp && signatures.any?
37
+
38
+ # Check timestamp to prevent replay attacks
39
+ return false unless timestamp_valid?(timestamp)
40
+
41
+ # Build signed payload: timestamp:raw_body
42
+ signed_payload = "#{timestamp}:#{raw_body}"
43
+
44
+ # Compute expected signature using HMAC-SHA256
45
+ expected_signature = compute_signature(signed_payload)
46
+
47
+ # Compare signatures (use timing-safe comparison)
48
+ signatures.any? { |sig| timing_safe_compare(sig, expected_signature) }
49
+ rescue StandardError => e
50
+ Rails.logger.error("PaddleRails::WebhookVerifier: Error verifying signature: #{e.message}")
51
+ false
52
+ end
53
+
54
+ private
55
+
56
+ # Parse the Paddle-Signature header.
57
+ #
58
+ # Format: "ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151"
59
+ #
60
+ # @param signature_header [String] The Paddle-Signature header value
61
+ # @return [Array<Integer, Array<String>>] [timestamp, array_of_signatures]
62
+ def parse_signature_header(signature_header)
63
+ parts = signature_header.split(";")
64
+ timestamp = nil
65
+ signatures = []
66
+
67
+ parts.each do |part|
68
+ key, value = part.split("=", 2)
69
+ case key
70
+ when "ts"
71
+ timestamp = value.to_i
72
+ when "h1"
73
+ signatures << value
74
+ end
75
+ end
76
+
77
+ [timestamp, signatures]
78
+ end
79
+
80
+ # Check if the timestamp is within the tolerance window.
81
+ #
82
+ # @param timestamp [Integer] Unix timestamp from the webhook
83
+ # @return [Boolean] true if timestamp is valid
84
+ def timestamp_valid?(timestamp)
85
+ current_time = Time.now.to_i
86
+ (current_time - timestamp).abs <= @timestamp_tolerance
87
+ end
88
+
89
+ # Compute HMAC-SHA256 signature for the signed payload.
90
+ #
91
+ # @param signed_payload [String] The timestamp:raw_body string
92
+ # @return [String] Hexadecimal signature
93
+ def compute_signature(signed_payload)
94
+ OpenSSL::HMAC.hexdigest("SHA256", @secret_key, signed_payload)
95
+ end
96
+
97
+ # Timing-safe string comparison to prevent timing attacks.
98
+ #
99
+ # @param a [String] First string
100
+ # @param b [String] Second string
101
+ # @return [Boolean] true if strings are equal
102
+ def timing_safe_compare(a, b)
103
+ return false if a.nil? || b.nil?
104
+ return false unless a.length == b.length
105
+
106
+ OpenSSL.fixed_length_secure_compare(a, b)
107
+ end
108
+ end
109
+ end
110
+
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PaddleRails is a zero-hassle Paddle subscription integration for Rails
4
+ # using custom_data-based identity.
5
+ #
6
+ # @example Basic usage
7
+ # # In config/initializers/paddle_rails.rb
8
+ # PaddleRails.configure do |config|
9
+ # config.api_key = ENV["PADDLE_API_KEY"]
10
+ # config.subscription_owner_authenticator do
11
+ # current_user || warden.authenticate!(scope: :user)
12
+ # end
13
+ # end
14
+ #
15
+ # @example Making a model subscribable
16
+ # class User < ApplicationRecord
17
+ # include PaddleRails::Subscribable
18
+ # end
19
+ #
20
+ # @see https://github.com/kjellberg/paddle_rails
21
+ module PaddleRails
22
+ end
23
+
24
+ require "paddle"
25
+ require "paddle_rails/version"
26
+ require "paddle_rails/engine"
27
+ require "paddle_rails/configuration"
28
+ require "paddle_rails/product_sync"
29
+ require "paddle_rails/checkout"
30
+ require "paddle_rails/webhook_verifier"
31
+ require "paddle_rails/webhook_processor"
32
+ require "paddle_rails/subscription_sync"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rake tasks for PaddleRails gem.
4
+ #
5
+ # @note This file currently contains placeholder tasks.
6
+ # Actual rake tasks will be added as needed.
7
+ #
8
+ # @example Running a task
9
+ # rake paddle_rails:sync_products
10
+ #
11
+ # @see PaddleRails::ProductSync
12
+ # desc "Explaining what the task does"
13
+ # task :paddle_rails do
14
+ # # Task goes here
15
+ # end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paddle_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rasmus Kjellberg
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.1.1
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.1.1
26
+ - !ruby/object:Gem::Dependency
27
+ name: paddle
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ description: |
41
+ PaddleRails is a production-ready Rails engine that drops a complete subscription management portal into your application in minutes.
42
+
43
+ It's not just an API wrapper—it's a full-stack billing solution that handles the hard parts of SaaS payments: webhooks, plan upgrades, prorations, cancellation flows, and payment method updates. Fully compliant with Paddle Billing (v2), handling global tax/VAT and localized pricing automatically.
44
+
45
+ Features:
46
+ - Mountable billing dashboard ready in minutes
47
+ - Built for Paddle Billing V2
48
+ - Two-way sync via webhooks
49
+ - Beautiful UI built with Tailwind CSS
50
+ - Uses GlobalID for bulletproof user mapping (no email mismatch issues)
51
+ - Supports subscriptions on Users, Teams, Organizations, or Tenants
52
+ - Payment history with invoice viewing and download
53
+ - Plan upgrades/downgrades with proration
54
+ - Payment method management
55
+
56
+ No need to build billing UI from scratch. Just mount the engine and focus on your product.
57
+ email:
58
+ - 2277443+kjellberg@users.noreply.github.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - app/assets/stylesheets/paddle_rails/application.css
67
+ - app/assets/stylesheets/paddle_rails/tailwind.css
68
+ - app/assets/tailwind/application.css
69
+ - app/controllers/concerns/paddle_rails/paddle_checkout_error_handler.rb
70
+ - app/controllers/concerns/paddle_rails/subscription_owner.rb
71
+ - app/controllers/paddle_rails/application_controller.rb
72
+ - app/controllers/paddle_rails/checkout_controller.rb
73
+ - app/controllers/paddle_rails/dashboard_controller.rb
74
+ - app/controllers/paddle_rails/onboarding_controller.rb
75
+ - app/controllers/paddle_rails/payments_controller.rb
76
+ - app/controllers/paddle_rails/subscriptions_controller.rb
77
+ - app/controllers/paddle_rails/webhooks_controller.rb
78
+ - app/helpers/paddle_rails/application_helper.rb
79
+ - app/helpers/paddle_rails/subscription_owner_helper.rb
80
+ - app/jobs/paddle_rails/application_job.rb
81
+ - app/jobs/paddle_rails/process_webhook_job.rb
82
+ - app/mailers/paddle_rails/application_mailer.rb
83
+ - app/models/concerns/paddle_rails/subscribable.rb
84
+ - app/models/paddle_rails/application_record.rb
85
+ - app/models/paddle_rails/payment.rb
86
+ - app/models/paddle_rails/price.rb
87
+ - app/models/paddle_rails/product.rb
88
+ - app/models/paddle_rails/subscription.rb
89
+ - app/models/paddle_rails/subscription_item.rb
90
+ - app/models/paddle_rails/webhook_event.rb
91
+ - app/presenters/paddle_rails/payment_presenter.rb
92
+ - app/presenters/paddle_rails/product_presenter.rb
93
+ - app/presenters/paddle_rails/subscription_presenter.rb
94
+ - app/views/layouts/paddle_rails/application.html.erb
95
+ - app/views/paddle_rails/checkout/show.html.erb
96
+ - app/views/paddle_rails/dashboard/_change_plan.html.erb
97
+ - app/views/paddle_rails/dashboard/_current_subscription.html.erb
98
+ - app/views/paddle_rails/dashboard/_payment_history.html.erb
99
+ - app/views/paddle_rails/dashboard/_payment_method.html.erb
100
+ - app/views/paddle_rails/dashboard/show.html.erb
101
+ - app/views/paddle_rails/onboarding/show.html.erb
102
+ - app/views/paddle_rails/shared/configuration_error.html.erb
103
+ - config/routes.rb
104
+ - db/migrate/20251124180624_create_paddle_rails_subscription_plans.rb
105
+ - db/migrate/20251124180817_create_paddle_rails_subscription_prices.rb
106
+ - db/migrate/20251127221947_create_paddle_rails_webhook_events.rb
107
+ - db/migrate/20251128135831_create_paddle_rails_subscriptions.rb
108
+ - db/migrate/20251128142327_create_paddle_rails_subscription_items.rb
109
+ - db/migrate/20251128151334_remove_paddle_price_id_from_subscriptions.rb
110
+ - db/migrate/20251128151401_rename_subscription_plans_to_products.rb
111
+ - db/migrate/20251128151402_rename_subscription_plan_id_to_subscription_product_id.rb
112
+ - db/migrate/20251128151453_remove_subscription_price_id_from_subscriptions.rb
113
+ - db/migrate/20251128151501_add_subscription_product_id_to_subscription_items.rb
114
+ - db/migrate/20251128152025_remove_paddle_item_id_from_subscription_items.rb
115
+ - db/migrate/20251128212046_rename_subscription_products_to_products.rb
116
+ - db/migrate/20251128212047_rename_subscription_prices_to_prices.rb
117
+ - db/migrate/20251128212053_rename_subscription_product_id_to_product_id_in_prices.rb
118
+ - db/migrate/20251128212054_rename_fks_in_subscription_items.rb
119
+ - db/migrate/20251128220016_add_scheduled_cancelation_at_to_subscriptions.rb
120
+ - db/migrate/20251129121336_add_payment_method_to_subscriptions.rb
121
+ - db/migrate/20251129222345_create_paddle_rails_payments.rb
122
+ - lib/paddle_rails.rb
123
+ - lib/paddle_rails/checkout.rb
124
+ - lib/paddle_rails/configuration.rb
125
+ - lib/paddle_rails/engine.rb
126
+ - lib/paddle_rails/product_sync.rb
127
+ - lib/paddle_rails/subscription_sync.rb
128
+ - lib/paddle_rails/version.rb
129
+ - lib/paddle_rails/webhook_processor.rb
130
+ - lib/paddle_rails/webhook_verifier.rb
131
+ - lib/tasks/paddle_rails_tasks.rake
132
+ homepage: https://github.com/kjellberg/paddle_rails
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ homepage_uri: https://github.com/kjellberg/paddle_rails
137
+ source_code_uri: https://github.com/kjellberg/paddle_rails
138
+ changelog_uri: https://github.com/kjellberg/paddle_rails/blob/main/CHANGELOG.md
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubygems_version: 3.6.7
154
+ specification_version: 4
155
+ summary: Plug-and-play billing engine for Rails + Paddle. Full subscription management
156
+ portal with webhooks, plan changes, and payment history.
157
+ test_files: []