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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e582d1858144111153c25ef93bd2e879139574ad812c22f91f5f2bc57ecc7b68
4
+ data.tar.gz: a2df8aae57a9ca5b48e0aea05254b225d45d3e373bdf01cbc55a7cc08475f35a
5
+ SHA512:
6
+ metadata.gz: f2a9b2b04f3e4f53f48cda7fbe01c9d659267a31ab1782ecc49360ace956bf499e10c712f2c6c1ae2b2e26e29fa605e26bc810d4f5b601f48d8b1122e2ab196c
7
+ data.tar.gz: 986d887b59ff62eec3bea43271c38e34cafc33865d223b8d3090cf2c564cd4619169adcdff316911e32a27b370ae5e4d23bd940c41837495576a2c8bb42a64d2
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Rasmus Kjellberg
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,294 @@
1
+ # PaddleRails
2
+
3
+ **The ultimate plug-and-play billing engine for Ruby on Rails and Paddle.**
4
+
5
+ Building a SaaS? Stop wasting weeks on billing integration. **PaddleRails** is a production-ready Rails engine that drops a complete subscription management portal into your application in minutes.
6
+
7
+ 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.
8
+
9
+ No need to build billing UI from scratch. Just mount the engine and focus on your product.
10
+
11
+ - ✅ **No email mismatches** - Users can change email in Paddle checkout without breaking your app
12
+ - ✅ **No customer collisions** - Multiple users can share billing emails safely
13
+ - ✅ **No complex customer lookups** - Identity is always resolved via secure custom_data payloads
14
+ - ✅ **No Pay gem dependency** - Clean, simple implementation without external abstractions
15
+
16
+ Instead of syncing users via Paddle Customers, the gem uses a secure custom_data hash (GlobalID) as the single source of truth. This means your app always links subscriptions to the correct user, regardless of email changes or shared billing addresses.
17
+
18
+ - 🚀 **Instant SaaS Infrastructure** — Mountable billing dashboard ready in minutes
19
+ - 💳 **Paddle Billing V2** — Built for the latest Paddle API
20
+ - 🔄 **Two-Way Sync** — Webhooks keep your local database perfectly synchronized
21
+ - 🎨 **Beautiful UI** — Modern, responsive dashboard built with Tailwind CSS
22
+ - 🛡 **Identity Secure** — Uses GlobalID for bulletproof user mapping (no email mismatch issues)
23
+ - 🏢 **B2B Ready** — Supports subscriptions on Users, Teams, Organizations, or Tenants
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Install the gem
28
+
29
+ ```ruby
30
+ # Gemfile
31
+ gem "paddle_rails"
32
+ ```
33
+
34
+ ```bash
35
+ $ bundle install
36
+ ```
37
+
38
+ Install the migrations to create the necessary tables:
39
+
40
+ ```bash
41
+ $ bin/rails paddle_rails:install:migrations
42
+ $ bin/rails db:migrate
43
+ ```
44
+
45
+ ### 2. Configure Paddle credentials
46
+
47
+ ```bash
48
+ # .env or environment variables
49
+ PADDLE_API_KEY=your_api_key
50
+ PADDLE_PUBLIC_TOKEN=your_public_token
51
+ PADDLE_ENVIRONMENT=sandbox # or "production"
52
+ PADDLE_WEBHOOK_SECRET=your_webhook_secret
53
+ ```
54
+
55
+ ### 3. Mount the billing portal
56
+
57
+ Add this to your `config/routes.rb`:
58
+
59
+ ```ruby
60
+ mount PaddleRails::Engine => "/billing"
61
+ ```
62
+
63
+ ### 4. Make your model subscribable
64
+
65
+ You can add subscription capabilities to **any** model—`User`, `Organization`, `Team`, `Tenant`, or `Domain`.
66
+
67
+ ```ruby
68
+ class User < ApplicationRecord
69
+ include PaddleRails::Subscribable
70
+ end
71
+ ```
72
+
73
+ ### 5. Sync your Paddle products
74
+
75
+ To import your products and prices from Paddle, add the sync command to your `db/seeds.rb`:
76
+
77
+ ```ruby
78
+ # db/seeds.rb
79
+ puts "Syncing Paddle products..."
80
+ PaddleRails::ProductSync.call
81
+ ```
82
+
83
+ Then run:
84
+
85
+ ```bash
86
+ $ bin/rails db:seed
87
+ ```
88
+
89
+ ### 6. Configure webhooks in Paddle
90
+
91
+ Go to your [Paddle notification settings](https://vendors.paddle.com/notifications-v2) and add:
92
+
93
+ - **URL**: `https://yourdomain.com/paddle_rails/webhooks`
94
+ - **Events**: `subscription.created`, `subscription.updated`, `subscription.canceled`, `transaction.completed`
95
+
96
+ **That's it!** Visit `/billing` to see your billing portal.
97
+
98
+ ## The Billing Portal
99
+
100
+ Once mounted, PaddleRails provides these pages automatically:
101
+
102
+ | Path | Description |
103
+ |------|-------------|
104
+ | `/billing` | Main dashboard showing current subscription, payment method, and billing info |
105
+ | `/billing/onboarding` | Plan selection page for new subscribers |
106
+ | `/billing/checkout` | Inline Paddle checkout experience |
107
+
108
+ ### Dashboard Features
109
+
110
+ - **Current Subscription** — Shows plan name, price, billing cycle, and next billing date
111
+ - **Subscription Status** — Active, trialing, canceled, or scheduled for cancellation
112
+ - **Payment Method** — Card brand, last 4 digits, expiration with update button
113
+ - **Cancel Subscription** — Schedule cancellation at end of billing period
114
+ - **Revoke Cancellation** — One-click to undo a pending cancellation
115
+
116
+ ### Screenshots
117
+
118
+ The billing portal is designed to be clean and professional out of the box. It uses Tailwind CSS classes and can be customized by overriding the views.
119
+
120
+ ## Configuration
121
+
122
+ ```ruby
123
+ # config/initializers/paddle_rails.rb
124
+ PaddleRails.configure do |config|
125
+ # Paddle API credentials
126
+ config.api_key = ENV["PADDLE_API_KEY"]
127
+ config.public_token = ENV["PADDLE_PUBLIC_TOKEN"]
128
+ config.environment = ENV["PADDLE_ENVIRONMENT"] # "sandbox" or "production"
129
+ config.webhook_secret = ENV["PADDLE_WEBHOOK_SECRET"]
130
+
131
+ # How to identify the subscription owner in the billing portal
132
+ config.subscription_owner_authenticator do
133
+ current_user || warden.authenticate!(scope: :user)
134
+ end
135
+
136
+ # Where the "Back" link goes in the portal
137
+ config.customer_portal_back_path do
138
+ main_app.root_path
139
+ end
140
+ end
141
+ ```
142
+
143
+ ### Paddle Dashboard Settings
144
+
145
+ #### Default Payment Link
146
+
147
+ When customers update their payment method, Paddle redirects them back to your app. Configure the **Default payment link** in your Paddle dashboard:
148
+
149
+ 1. Go to **Checkout Settings**:
150
+ - Sandbox: https://sandbox-vendors.paddle.com/checkout-settings
151
+ - Production: https://vendors.paddle.com/checkout-settings
152
+
153
+ 2. Set **Default payment link** to your billing dashboard URL:
154
+ ```
155
+ https://yourdomain.com/billing
156
+ ```
157
+
158
+ ## How It Works
159
+
160
+ ### Identity via custom_data
161
+
162
+ PaddleRails uses a secure approach to link Paddle subscriptions to your users:
163
+
164
+ 1. When creating a checkout, the gem injects a signed GlobalID into `custom_data`
165
+ 2. When webhooks arrive, this ID is used to find the correct user
166
+ 3. No reliance on email matching or Paddle customer IDs
167
+
168
+ This means:
169
+ - Users can change their email in Paddle checkout without breaking anything
170
+ - Multiple users can share billing emails safely
171
+ - Identity is always resolved correctly
172
+
173
+ ### Automatic Sync
174
+
175
+ The gem automatically keeps your local database in sync with Paddle:
176
+
177
+ - **Products & Prices** — Use `PaddleRails::ProductSync.call` to mirror your Paddle catalog
178
+ - **Subscriptions** — Webhook handlers automatically create/update subscription records
179
+ - **Payment Methods** — Updated automatically when transactions complete
180
+
181
+ ## Checking Subscription Status
182
+
183
+ ```ruby
184
+ # In your controllers or views
185
+ if current_user.subscription?
186
+ # User has an active subscription
187
+ end
188
+
189
+ # More detailed checks
190
+ current_user.subscription.active?
191
+ current_user.subscription.trialing?
192
+ current_user.subscription.scheduled_for_cancellation?
193
+ ```
194
+
195
+ ## Customization
196
+
197
+ ### Override Views
198
+
199
+ Copy the views to your application to customize:
200
+
201
+ ```bash
202
+ $ bin/rails generate paddle_rails:views
203
+ ```
204
+
205
+ This copies all views to `app/views/paddle_rails/` where you can modify them.
206
+
207
+ ### Custom Webhook Handling
208
+
209
+ Listen to Paddle events using ActiveSupport::Notifications:
210
+
211
+ ```ruby
212
+ # config/initializers/paddle_rails.rb
213
+ ActiveSupport::Notifications.subscribe("paddle_rails.subscription.created") do |name, start, finish, id, payload|
214
+ webhook_event = payload[:webhook_event]
215
+ # Send welcome email, provision resources, etc.
216
+ end
217
+
218
+ ActiveSupport::Notifications.subscribe("paddle_rails.subscription.canceled") do |name, start, finish, id, payload|
219
+ # Handle cancellation
220
+ end
221
+ ```
222
+
223
+ ### Programmatic Checkout
224
+
225
+ Create checkouts from your own code:
226
+
227
+ ```ruby
228
+ # Simple checkout
229
+ checkout_url = current_user.create_paddle_checkout(paddle_price_id: "pri_123")
230
+ redirect_to checkout_url, allow_other_host: true
231
+
232
+ # With custom data
233
+ checkout_url = current_user.create_paddle_checkout(
234
+ paddle_price_id: "pri_123",
235
+ custom_data: { referral_code: "ABC123" }
236
+ )
237
+ ```
238
+
239
+ ## Models
240
+
241
+ ### PaddleRails::Subscription
242
+
243
+ ```ruby
244
+ subscription = current_user.subscription
245
+
246
+ subscription.active? # Currently active
247
+ subscription.trialing? # In trial period
248
+ subscription.scheduled_for_cancellation? # Will cancel at period end
249
+ subscription.current_period_end_at # Next billing date
250
+ subscription.items # All subscription items (for multi-product)
251
+ ```
252
+
253
+ ### PaddleRails::Product & PaddleRails::Price
254
+
255
+ ```ruby
256
+ # Your Paddle products, synced locally
257
+ PaddleRails::Product.active.each do |product|
258
+ product.name
259
+ product.prices.each do |price|
260
+ price.unit_price # In minor units (cents)
261
+ price.currency
262
+ price.billing_interval # "month", "year"
263
+ end
264
+ end
265
+ ```
266
+
267
+ ## Webhook Events
268
+
269
+ All webhooks are stored in `PaddleRails::WebhookEvent` for debugging and replayability:
270
+
271
+ ```ruby
272
+ PaddleRails::WebhookEvent.pending # Not yet processed
273
+ PaddleRails::WebhookEvent.processed # Successfully handled
274
+ PaddleRails::WebhookEvent.failed # Had errors
275
+
276
+ event = PaddleRails::WebhookEvent.last
277
+ event.event_type # "subscription.created"
278
+ event.payload # Full JSON from Paddle
279
+ ```
280
+
281
+ ## Development
282
+
283
+ ```bash
284
+ $ bin/setup
285
+ $ bin/rails test
286
+ ```
287
+
288
+ ## Contributing
289
+
290
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kjellberg/paddle_rails.
291
+
292
+ ## License
293
+
294
+ 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,6 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
@@ -0,0 +1,16 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+