nocheckout 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9db06290291d396f599cd4480858528a842f818be68f58e154ad7e5f5f8b6299
4
- data.tar.gz: '06974973a6ac44a4c909e48bc82737c3f16639981016d3ee9e4b815c74b38203'
3
+ metadata.gz: 36cc99edab4e1790d54150c8528af4a8909e9894c08716ee9d6893a4966772da
4
+ data.tar.gz: e909fba5b3d5dff31373ef0f268fabf3fa53297ea77d1fd09f7f13ad5147e541
5
5
  SHA512:
6
- metadata.gz: 4d900112610418b2b15882d3e2b58ba13bd6ced82e6fa01dbcfa84442436a941fb1454a754c8e2b5e76d2ef861bf7abf3653f3b02a4f6ecfc297d5d9e7ab8995
7
- data.tar.gz: 44e17d5ff40f48f7744711871cbb68ef9522248e521a2e0743b66486458f4ae4be6a15b41ab5bc0826b16534c168566234aa94a77d633db91bbb5e7ec7ebd38b
6
+ metadata.gz: b03567f074c779298ed5d3ae1b2e3bf81146b169f7002ce81ab6b8a75e399dd26c8b54833cdc3469cbe636c368365d5225c30d191341a317b22beee1f411018a
7
+ data.tar.gz: 05e63b688befdcf2f12274e7b5daac3d9ff07cf8f90749d86e08cc952947d98773d5bc9e3019f3d44d902bcbe9eb0be441430952a8101dd0cbc6ac93a394b67c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Changed
4
+ - **BREAKING**: Refactored `CheckoutSessionsController` from a base class to a composable module
5
+ - `NoCheckout::Stripe::CheckoutSession` is now a concern/module that you include in your controllers
6
+ - Controllers no longer need to inherit from `NoCheckout::Stripe::CheckoutSessionsController`
7
+ - Provides more flexibility - controllers can inherit from any base class in your app
8
+ - All protected methods (`create_checkout_session`, `retrieve_checkout_session`, `callback_url`, etc.) remain the same
9
+ - `@checkout_session` is still automatically assigned via before_action callbacks
10
+ - See README for migration examples
11
+
12
+ ### Added
13
+ - Comprehensive test suite for `CheckoutSession` module and `CheckoutSessionsController`
14
+ - Architecture documentation in README explaining the module-based design pattern
15
+
3
16
  ## [0.1.0] - 2023-09-08
4
17
 
5
18
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,95 +1,108 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nocheckout (0.1.2)
5
- rails (~> 7.0)
4
+ nocheckout (0.1.4)
5
+ rails (>= 7.0)
6
6
  stripe (>= 7.0)
7
7
  zeitwerk (~> 2.0)
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actioncable (7.0.8)
13
- actionpack (= 7.0.8)
14
- activesupport (= 7.0.8)
12
+ actioncable (7.2.1)
13
+ actionpack (= 7.2.1)
14
+ activesupport (= 7.2.1)
15
15
  nio4r (~> 2.0)
16
16
  websocket-driver (>= 0.6.1)
17
- actionmailbox (7.0.8)
18
- actionpack (= 7.0.8)
19
- activejob (= 7.0.8)
20
- activerecord (= 7.0.8)
21
- activestorage (= 7.0.8)
22
- activesupport (= 7.0.8)
23
- mail (>= 2.7.1)
24
- net-imap
25
- net-pop
26
- net-smtp
27
- actionmailer (7.0.8)
28
- actionpack (= 7.0.8)
29
- actionview (= 7.0.8)
30
- activejob (= 7.0.8)
31
- activesupport (= 7.0.8)
32
- mail (~> 2.5, >= 2.5.4)
33
- net-imap
34
- net-pop
35
- net-smtp
36
- rails-dom-testing (~> 2.0)
37
- actionpack (7.0.8)
38
- actionview (= 7.0.8)
39
- activesupport (= 7.0.8)
40
- rack (~> 2.0, >= 2.2.4)
17
+ zeitwerk (~> 2.6)
18
+ actionmailbox (7.2.1)
19
+ actionpack (= 7.2.1)
20
+ activejob (= 7.2.1)
21
+ activerecord (= 7.2.1)
22
+ activestorage (= 7.2.1)
23
+ activesupport (= 7.2.1)
24
+ mail (>= 2.8.0)
25
+ actionmailer (7.2.1)
26
+ actionpack (= 7.2.1)
27
+ actionview (= 7.2.1)
28
+ activejob (= 7.2.1)
29
+ activesupport (= 7.2.1)
30
+ mail (>= 2.8.0)
31
+ rails-dom-testing (~> 2.2)
32
+ actionpack (7.2.1)
33
+ actionview (= 7.2.1)
34
+ activesupport (= 7.2.1)
35
+ nokogiri (>= 1.8.5)
36
+ racc
37
+ rack (>= 2.2.4, < 3.2)
38
+ rack-session (>= 1.0.1)
41
39
  rack-test (>= 0.6.3)
42
- rails-dom-testing (~> 2.0)
43
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
44
- actiontext (7.0.8)
45
- actionpack (= 7.0.8)
46
- activerecord (= 7.0.8)
47
- activestorage (= 7.0.8)
48
- activesupport (= 7.0.8)
40
+ rails-dom-testing (~> 2.2)
41
+ rails-html-sanitizer (~> 1.6)
42
+ useragent (~> 0.16)
43
+ actiontext (7.2.1)
44
+ actionpack (= 7.2.1)
45
+ activerecord (= 7.2.1)
46
+ activestorage (= 7.2.1)
47
+ activesupport (= 7.2.1)
49
48
  globalid (>= 0.6.0)
50
49
  nokogiri (>= 1.8.5)
51
- actionview (7.0.8)
52
- activesupport (= 7.0.8)
50
+ actionview (7.2.1)
51
+ activesupport (= 7.2.1)
53
52
  builder (~> 3.1)
54
- erubi (~> 1.4)
55
- rails-dom-testing (~> 2.0)
56
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
57
- activejob (7.0.8)
58
- activesupport (= 7.0.8)
53
+ erubi (~> 1.11)
54
+ rails-dom-testing (~> 2.2)
55
+ rails-html-sanitizer (~> 1.6)
56
+ activejob (7.2.1)
57
+ activesupport (= 7.2.1)
59
58
  globalid (>= 0.3.6)
60
- activemodel (7.0.8)
61
- activesupport (= 7.0.8)
62
- activerecord (7.0.8)
63
- activemodel (= 7.0.8)
64
- activesupport (= 7.0.8)
65
- activestorage (7.0.8)
66
- actionpack (= 7.0.8)
67
- activejob (= 7.0.8)
68
- activerecord (= 7.0.8)
69
- activesupport (= 7.0.8)
59
+ activemodel (7.2.1)
60
+ activesupport (= 7.2.1)
61
+ activerecord (7.2.1)
62
+ activemodel (= 7.2.1)
63
+ activesupport (= 7.2.1)
64
+ timeout (>= 0.4.0)
65
+ activestorage (7.2.1)
66
+ actionpack (= 7.2.1)
67
+ activejob (= 7.2.1)
68
+ activerecord (= 7.2.1)
69
+ activesupport (= 7.2.1)
70
70
  marcel (~> 1.0)
71
- mini_mime (>= 1.1.0)
72
- activesupport (7.0.8)
73
- concurrent-ruby (~> 1.0, >= 1.0.2)
71
+ activesupport (7.2.1)
72
+ base64
73
+ bigdecimal
74
+ concurrent-ruby (~> 1.0, >= 1.3.1)
75
+ connection_pool (>= 2.2.5)
76
+ drb
74
77
  i18n (>= 1.6, < 2)
78
+ logger (>= 1.4.2)
75
79
  minitest (>= 5.1)
76
- tzinfo (~> 2.0)
80
+ securerandom (>= 0.3)
81
+ tzinfo (~> 2.0, >= 2.0.5)
77
82
  ast (2.4.2)
78
- base64 (0.1.1)
79
- builder (3.2.4)
80
- concurrent-ruby (1.2.2)
83
+ base64 (0.2.0)
84
+ bigdecimal (3.1.8)
85
+ builder (3.3.0)
86
+ concurrent-ruby (1.3.4)
87
+ connection_pool (2.4.1)
81
88
  crass (1.0.6)
82
- date (3.3.3)
83
- diff-lcs (1.5.0)
84
- erubi (1.12.0)
89
+ date (3.3.4)
90
+ diff-lcs (1.5.1)
91
+ drb (2.2.1)
92
+ erubi (1.13.0)
85
93
  globalid (1.2.1)
86
94
  activesupport (>= 6.1)
87
- i18n (1.14.1)
95
+ i18n (1.14.6)
88
96
  concurrent-ruby (~> 1.0)
89
- json (2.6.3)
97
+ io-console (0.7.2)
98
+ irb (1.14.1)
99
+ rdoc (>= 4.0.0)
100
+ reline (>= 0.4.2)
101
+ json (2.7.2)
90
102
  language_server-protocol (3.17.0.3)
91
103
  lint_roller (1.1.0)
92
- loofah (2.21.3)
104
+ logger (1.6.1)
105
+ loofah (2.22.0)
93
106
  crass (~> 1.0.2)
94
107
  nokogiri (>= 1.12.0)
95
108
  mail (2.8.1)
@@ -97,46 +110,56 @@ GEM
97
110
  net-imap
98
111
  net-pop
99
112
  net-smtp
100
- marcel (1.0.2)
101
- method_source (1.0.0)
113
+ marcel (1.0.4)
102
114
  mini_mime (1.1.5)
103
- minitest (5.20.0)
104
- net-imap (0.3.7)
115
+ mini_portile2 (2.8.9)
116
+ minitest (5.25.1)
117
+ net-imap (0.4.16)
105
118
  date
106
119
  net-protocol
107
120
  net-pop (0.1.2)
108
121
  net-protocol
109
- net-protocol (0.2.1)
122
+ net-protocol (0.2.2)
110
123
  timeout
111
- net-smtp (0.3.3)
124
+ net-smtp (0.5.0)
112
125
  net-protocol
113
- nio4r (2.5.9)
114
- nokogiri (1.15.4-arm64-darwin)
126
+ nio4r (2.7.3)
127
+ nokogiri (1.16.7)
128
+ mini_portile2 (~> 2.8.2)
129
+ racc (~> 1.4)
130
+ nokogiri (1.16.7-arm64-darwin)
115
131
  racc (~> 1.4)
116
- nokogiri (1.15.4-x86_64-linux)
132
+ nokogiri (1.16.7-x86_64-linux)
117
133
  racc (~> 1.4)
118
- parallel (1.23.0)
119
- parser (3.2.2.3)
134
+ parallel (1.26.3)
135
+ parser (3.3.5.0)
120
136
  ast (~> 2.4.1)
121
137
  racc
122
- racc (1.7.1)
123
- rack (2.2.8)
138
+ psych (5.1.2)
139
+ stringio
140
+ racc (1.8.1)
141
+ rack (3.1.7)
142
+ rack-session (2.0.0)
143
+ rack (>= 3.0.0)
124
144
  rack-test (2.1.0)
125
145
  rack (>= 1.3)
126
- rails (7.0.8)
127
- actioncable (= 7.0.8)
128
- actionmailbox (= 7.0.8)
129
- actionmailer (= 7.0.8)
130
- actionpack (= 7.0.8)
131
- actiontext (= 7.0.8)
132
- actionview (= 7.0.8)
133
- activejob (= 7.0.8)
134
- activemodel (= 7.0.8)
135
- activerecord (= 7.0.8)
136
- activestorage (= 7.0.8)
137
- activesupport (= 7.0.8)
146
+ rackup (2.1.0)
147
+ rack (>= 3)
148
+ webrick (~> 1.8)
149
+ rails (7.2.1)
150
+ actioncable (= 7.2.1)
151
+ actionmailbox (= 7.2.1)
152
+ actionmailer (= 7.2.1)
153
+ actionpack (= 7.2.1)
154
+ actiontext (= 7.2.1)
155
+ actionview (= 7.2.1)
156
+ activejob (= 7.2.1)
157
+ activemodel (= 7.2.1)
158
+ activerecord (= 7.2.1)
159
+ activestorage (= 7.2.1)
160
+ activesupport (= 7.2.1)
138
161
  bundler (>= 1.15.0)
139
- railties (= 7.0.8)
162
+ railties (= 7.2.1)
140
163
  rails-dom-testing (2.2.0)
141
164
  activesupport (>= 5.0.0)
142
165
  minitest
@@ -144,73 +167,83 @@ GEM
144
167
  rails-html-sanitizer (1.6.0)
145
168
  loofah (~> 2.21)
146
169
  nokogiri (~> 1.14)
147
- railties (7.0.8)
148
- actionpack (= 7.0.8)
149
- activesupport (= 7.0.8)
150
- method_source
170
+ railties (7.2.1)
171
+ actionpack (= 7.2.1)
172
+ activesupport (= 7.2.1)
173
+ irb (~> 1.13)
174
+ rackup (>= 1.0.0)
151
175
  rake (>= 12.2)
152
- thor (~> 1.0)
153
- zeitwerk (~> 2.5)
176
+ thor (~> 1.0, >= 1.2.2)
177
+ zeitwerk (~> 2.6)
154
178
  rainbow (3.1.1)
155
- rake (13.0.6)
156
- regexp_parser (2.8.1)
157
- rexml (3.2.6)
158
- rspec (3.12.0)
159
- rspec-core (~> 3.12.0)
160
- rspec-expectations (~> 3.12.0)
161
- rspec-mocks (~> 3.12.0)
162
- rspec-core (3.12.2)
163
- rspec-support (~> 3.12.0)
164
- rspec-expectations (3.12.3)
179
+ rake (13.2.1)
180
+ rdoc (6.7.0)
181
+ psych (>= 4.0.0)
182
+ regexp_parser (2.9.2)
183
+ reline (0.5.10)
184
+ io-console (~> 0.5)
185
+ rexml (3.3.8)
186
+ rspec (3.13.0)
187
+ rspec-core (~> 3.13.0)
188
+ rspec-expectations (~> 3.13.0)
189
+ rspec-mocks (~> 3.13.0)
190
+ rspec-core (3.13.1)
191
+ rspec-support (~> 3.13.0)
192
+ rspec-expectations (3.13.3)
165
193
  diff-lcs (>= 1.2.0, < 2.0)
166
- rspec-support (~> 3.12.0)
167
- rspec-mocks (3.12.6)
194
+ rspec-support (~> 3.13.0)
195
+ rspec-mocks (3.13.2)
168
196
  diff-lcs (>= 1.2.0, < 2.0)
169
- rspec-support (~> 3.12.0)
170
- rspec-support (3.12.1)
171
- rubocop (1.56.3)
172
- base64 (~> 0.1.1)
197
+ rspec-support (~> 3.13.0)
198
+ rspec-support (3.13.1)
199
+ rubocop (1.65.1)
173
200
  json (~> 2.3)
174
201
  language_server-protocol (>= 3.17.0)
175
202
  parallel (~> 1.10)
176
- parser (>= 3.2.2.3)
203
+ parser (>= 3.3.0.2)
177
204
  rainbow (>= 2.2.2, < 4.0)
178
- regexp_parser (>= 1.8, < 3.0)
205
+ regexp_parser (>= 2.4, < 3.0)
179
206
  rexml (>= 3.2.5, < 4.0)
180
- rubocop-ast (>= 1.28.1, < 2.0)
207
+ rubocop-ast (>= 1.31.1, < 2.0)
181
208
  ruby-progressbar (~> 1.7)
182
209
  unicode-display_width (>= 2.4.0, < 3.0)
183
- rubocop-ast (1.29.0)
184
- parser (>= 3.2.1.0)
185
- rubocop-performance (1.19.0)
186
- rubocop (>= 1.7.0, < 2.0)
187
- rubocop-ast (>= 0.4.0)
210
+ rubocop-ast (1.32.3)
211
+ parser (>= 3.3.1.0)
212
+ rubocop-performance (1.21.1)
213
+ rubocop (>= 1.48.1, < 2.0)
214
+ rubocop-ast (>= 1.31.1, < 2.0)
188
215
  ruby-progressbar (1.13.0)
189
- standard (1.31.1)
216
+ securerandom (0.3.1)
217
+ standard (1.40.1)
190
218
  language_server-protocol (~> 3.17.0.2)
191
219
  lint_roller (~> 1.0)
192
- rubocop (~> 1.56.2)
220
+ rubocop (~> 1.65.0)
193
221
  standard-custom (~> 1.0.0)
194
- standard-performance (~> 1.2)
222
+ standard-performance (~> 1.4)
195
223
  standard-custom (1.0.2)
196
224
  lint_roller (~> 1.0)
197
225
  rubocop (~> 1.50)
198
- standard-performance (1.2.0)
226
+ standard-performance (1.4.0)
199
227
  lint_roller (~> 1.1)
200
- rubocop-performance (~> 1.19.0)
201
- stripe (9.2.0)
202
- thor (1.2.2)
203
- timeout (0.4.0)
228
+ rubocop-performance (~> 1.21.0)
229
+ stringio (3.1.1)
230
+ stripe (13.0.0)
231
+ thor (1.3.2)
232
+ timeout (0.4.1)
204
233
  tzinfo (2.0.6)
205
234
  concurrent-ruby (~> 1.0)
206
- unicode-display_width (2.4.2)
235
+ unicode-display_width (2.6.0)
236
+ useragent (0.16.10)
237
+ webrick (1.8.2)
207
238
  websocket-driver (0.7.6)
208
239
  websocket-extensions (>= 0.1.0)
209
240
  websocket-extensions (0.1.5)
210
- zeitwerk (2.6.11)
241
+ zeitwerk (2.6.18)
211
242
 
212
243
  PLATFORMS
213
244
  arm64-darwin-22
245
+ arm64-darwin-23
246
+ arm64-darwin-24
214
247
  x86_64-linux
215
248
 
216
249
  DEPENDENCIES
@@ -220,4 +253,4 @@ DEPENDENCIES
220
253
  standard (~> 1.3)
221
254
 
222
255
  BUNDLED WITH
223
- 2.4.8
256
+ 2.6.5
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # NoCheckout
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/nocheckout.svg)](https://badge.fury.io/rb/nocheckout) [![Tests](https://github.com/rubymonolith/nocheckout/actions/workflows/main.yml/badge.svg)](https://github.com/rubymonolith/nocheckout/actions/workflows/main.yml)
4
+
3
5
  > [!IMPORTANT]
4
6
  > This project is a work in progress. This README was written to better understand the implementation for developers. **Some of the APIs may not have yet been implemented, renamed, or removed**. When the API settles down and is running in production for a while, a 1.0 release will be cut and this notice removed.
5
7
 
@@ -28,54 +30,222 @@ Before you do anything you'll need to go to https://dashboard.stripe.com/test/ap
28
30
  Stripe.api_key = Rails.configuration.stripe[:secret_key]
29
31
  ```
30
32
 
31
- ## Usage
33
+ ## Checkout Sessions Controller
32
34
 
33
- This library comes with two controllers, both map closely to their respective Stripe docs.
35
+ [Stripe Checkout Sessions](https://stripe.com/docs/api/checkout/sessions) send users from your website to a branded stripe.com page where they can enter their credit card details and complete the purchase. Once the purchase is complete, the user is redirected back to your website.
34
36
 
35
- ### Checkout Sessions Controller
37
+ NoCheckout provides a `CheckoutSession` module that you can include in your controllers to handle the interface between Stripe and your Rails application. The module is designed to be minimal and flexible.
36
38
 
37
- [Stripe Checkout Sessions](https://stripe.com/docs/api/checkout/sessions) send users from your website to a branded stripe.com page where they can enter their credit card details and complete the purchase. Once the purchase is complete, the user is redirected back to your website.
39
+ To get started, create a controller and include the `NoCheckout::Stripe::CheckoutSession` module. The module provides:
40
+
41
+ - **`#new` action**: Creates a Checkout Session and redirects users to Stripe
42
+ - **`#show` action**: Handles the callback when users return from Stripe
43
+ - **Protected methods**: `create_checkout_session`, `retrieve_checkout_session`, `callback_url`, `success_url`, `cancel_url`
44
+
45
+ The module automatically sets up `before_action` callbacks that:
46
+ - Call `create_checkout_session` and assign it to `@checkout_session` before the `new` action
47
+ - Call `retrieve_checkout_session` and assign it to `@checkout_session` before the `show` action
38
48
 
39
- The NoCheckout::CheckoutSessionsController handles the interface between Stripe and your Rails application and tries to be as small as possible.
49
+ ### Create user record after checkout is complete
40
50
 
41
- To get started, create a base CheckoutSessionsController that maps the Users from your application with [Stripe Customers](https://stripe.com/docs/api/customers).
51
+ This approach creates a new user record after the checkout is complete with the name and email they give during the Stripe checkout process.
42
52
 
43
53
  ```ruby
44
- class CheckoutSessionsController < NoCheckout::Stripe::CheckoutSessionsController
45
- protected
46
- def customer_id
47
- user.id
48
- end
54
+ class PaymentsController < ApplicationController
55
+ include NoCheckout::Stripe::CheckoutSession
56
+
57
+ STRIPE_PRICE = "price_..."
49
58
 
50
- def create_customer
51
- Stripe::Customer.create(
52
- id: customer_id,
53
- name: user.name,
54
- email: user.email
55
- )
59
+ def show
60
+ # @checkout_session is automatically assigned by the module
61
+ customer = Stripe::Customer.retrieve @checkout_session.customer
62
+ subscription = Stripe::Subscription.retrieve @checkout_session.subscription
63
+
64
+ # Do stuff with Stripe info
65
+ user = User.find_or_create_by email: customer.email
66
+ customer.metadata.user_id = user.id
67
+ customer.save
68
+ user.name = customer.name
69
+ user.save!
70
+
71
+ # In this example we set the current user to stripe info. This likely
72
+ # doesn't make sense for your security context, so be careful...
73
+ self.current_user = user
74
+ redirect_to root_url
75
+ end
76
+
77
+ protected
78
+ def create_checkout_session
79
+ super \
80
+ mode: "subscription",
81
+ line_items: [{
82
+ price: self.class::STRIPE_PRICE,
83
+ quantity: 1
84
+ }]
56
85
  end
57
86
  end
58
87
  ```
59
88
 
60
- Then, for each product you want to offer, create a controller and inherit the `CheckoutSessionsController`.
89
+ Then, for each product you want to offer, create a controller and include the `CheckoutSession` module. You can also inherit from a base controller:
61
90
 
62
91
  ```ruby
63
92
  class PlusCheckoutSessionsController < PaymentsController
64
93
  STRIPE_PRICE = "price_..."
94
+ end
95
+ ```
96
+
97
+ Or include the module directly:
98
+
99
+ ```ruby
100
+ class ProCheckoutSessionsController < ApplicationController
101
+ include NoCheckout::Stripe::CheckoutSession
102
+
103
+ STRIPE_PRICE = "price_..."
104
+
105
+ def show
106
+ # Handle callback after successful checkout
107
+ redirect_to dashboard_url, notice: "Welcome to Pro!"
108
+ end
65
109
 
66
110
  protected
67
111
  def create_checkout_session
68
- create_stripe_checkout_session line_items: [{
69
- price: STRIPE_PRICE,
70
- quantity: 1
71
- }]
112
+ super \
113
+ mode: "subscription",
114
+ line_items: [{price: self.class::STRIPE_PRICE, quantity: 1}]
72
115
  end
73
116
  end
74
117
  ```
75
118
 
76
119
  There's a lot of different ways you can wire up the controllers depending on how many Stripe prices are in your application. This README assumes you're selling just a few products, so the prices are hard coded as constants in the controller. This could easily be populated from a database.
77
120
 
78
- ### Webhooks Controller
121
+ ### Create a user record before checkout is complete
122
+
123
+ ```ruby
124
+ class PaymentsController < ApplicationController
125
+ include NoCheckout::Stripe::CheckoutSession
126
+
127
+ before_action :authenticate_user! # Loads a current_user
128
+
129
+ STRIPE_PRICE = "price_..."
130
+
131
+ def show
132
+ # @checkout_session is automatically assigned by the module
133
+ customer = Stripe::Customer.retrieve @checkout_session.customer
134
+ subscription = Stripe::Subscription.retrieve @checkout_session.subscription
135
+
136
+ # Update user with subscription info
137
+ current_user.update!(
138
+ stripe_customer_id: customer.id,
139
+ stripe_subscription_id: subscription.id
140
+ )
141
+
142
+ redirect_to root_url, notice: "Subscription activated!"
143
+ end
144
+
145
+ protected
146
+ def create_checkout_session
147
+ # Find or create a Stripe customer for the current user
148
+ customer = if current_user.stripe_customer_id.present?
149
+ Stripe::Customer.retrieve(current_user.stripe_customer_id)
150
+ else
151
+ Stripe::Customer.create(
152
+ email: current_user.email,
153
+ name: current_user.name,
154
+ metadata: {user_id: current_user.id}
155
+ )
156
+ end
157
+
158
+ super \
159
+ mode: "subscription",
160
+ customer: customer.id,
161
+ line_items: [{
162
+ price: self.class::STRIPE_PRICE,
163
+ quantity: 1
164
+ }]
165
+ end
166
+ end
167
+ ```
168
+
169
+ ### Customizing callback URLs
170
+
171
+ By default, the module uses the same URL for both `success_url` and `cancel_url`, pointing to the `show` action with the Checkout Session ID. You can override these methods to customize the behavior:
172
+
173
+ ```ruby
174
+ class PaymentsController < ApplicationController
175
+ include NoCheckout::Stripe::CheckoutSession
176
+
177
+ protected
178
+ def success_url
179
+ # Custom success URL
180
+ callback_url(status: "success")
181
+ end
182
+
183
+ def cancel_url
184
+ # Custom cancel URL - could point to a different action or controller
185
+ pricing_url
186
+ end
187
+ end
188
+ ```
189
+
190
+ ### Routes
191
+
192
+ Don't forget to add routes for your checkout controllers:
193
+
194
+ ```ruby
195
+ # config/routes.rb
196
+ Rails.application.routes.draw do
197
+ resources :payments, controller: "payments", only: [:new, :show]
198
+
199
+ # Or for multiple products:
200
+ resource :plus_checkout, controller: "plus_checkout_sessions", only: [:new, :show]
201
+ resource :pro_checkout, controller: "pro_checkout_sessions", only: [:new, :show]
202
+ end
203
+ ```
204
+
205
+ ## Architecture
206
+
207
+ The `CheckoutSession` module is designed as a Rails concern that provides a minimal, composable interface for Stripe Checkout Sessions. Here's how it works:
208
+
209
+ ### Module-Based Design
210
+
211
+ Instead of inheriting from a base controller class, you **include** the `CheckoutSession` module into your own controllers. This gives you maximum flexibility:
212
+
213
+ - **No forced inheritance**: Your controllers can inherit from any base controller in your app
214
+ - **Composable**: Mix and match with other concerns and modules
215
+ - **Customizable**: Override any method to change behavior
216
+ - **Testable**: Each piece can be tested independently
217
+
218
+ ### What the Module Provides
219
+
220
+ 1. **Before Actions**: Automatically sets up `@checkout_session` for `new` and `show` actions
221
+ 2. **Action Methods**: Provides `#new` action that redirects to Stripe
222
+ 3. **Protected Methods**: Gives you `create_checkout_session`, `retrieve_checkout_session`, `callback_url`, `success_url`, and `cancel_url` to override as needed
223
+ 4. **Constants**: Includes helper constants for handling Stripe's callback parameter
224
+
225
+ ### Customization Pattern
226
+
227
+ The module follows a simple pattern:
228
+
229
+ ```ruby
230
+ class YourController < ApplicationController
231
+ include NoCheckout::Stripe::CheckoutSession
232
+
233
+ # 1. Override protected methods to customize Stripe session creation
234
+ protected
235
+ def create_checkout_session
236
+ super(mode: "payment", line_items: [...])
237
+ end
238
+
239
+ # 2. Implement show action to handle the callback
240
+ def show
241
+ # Access @checkout_session that was automatically retrieved
242
+ # Do your business logic
243
+ # Redirect user
244
+ end
245
+ end
246
+ ```
247
+
248
+ ## Webhooks Controller
79
249
 
80
250
  [Stripe Webhooks](https://stripe.com/docs/webhooks) are extensive and keep your application up-to-date with what Stripe. In this example, we'll look at how to handle a subscription that's expiring and update a User record in our database.
81
251
 
@@ -83,8 +253,6 @@ There's a lot of different ways you can wire up the controllers depending on how
83
253
  class StripesController < NoCheckout::Stripe::WebhooksController
84
254
  STRIPE_SIGNING_SECRET = ENV["STRIPE_SIGNING_SECRET"]
85
255
 
86
- protected
87
-
88
256
  def customer_subscription_created
89
257
  user.subscription_expires_at data.current_period_end
90
258
  end
@@ -0,0 +1,57 @@
1
+ module NoCheckout::Stripe
2
+ module CheckoutSession
3
+ extend ActiveSupport::Concern
4
+
5
+ # Unescaped placeholder for Stripe to insert the Checkout Session ID.
6
+ STRIPE_CALLBACK_PARAMETER = "{CHECKOUT_SESSION_ID}".freeze
7
+
8
+ # Escaped version that Rails will emit.
9
+ ESCAPED_STRIPE_CALLBACK_PARAMETER = CGI.escape(STRIPE_CALLBACK_PARAMETER).freeze
10
+
11
+ # Hoist the Stripe constant for easier access.
12
+ Stripe = ::Stripe
13
+
14
+ included do
15
+ before_action def assign_created_checkout_session
16
+ @checkout_session = create_checkout_session
17
+ end, only: :new
18
+
19
+ before_action def assign_retrieved_checkout_session
20
+ @checkout_session = retrieve_checkout_session
21
+ end, only: :show
22
+ end
23
+
24
+ # Creates a new Checkout Session and redirects to it.
25
+ def new
26
+ redirect_to @checkout_session.url, allow_other_host: true
27
+ end
28
+
29
+ protected
30
+
31
+ # Creates a Stripe Checkout Session with callback URLs appended.
32
+ def create_checkout_session(**)
33
+ Stripe::Checkout::Session.create(success_url:, cancel_url:, **)
34
+ end
35
+
36
+ # Retrieves an existing Stripe Checkout Session by ID.
37
+ def retrieve_checkout_session(*, **)
38
+ Stripe::Checkout::Session.retrieve params.fetch(:id), *, **
39
+ end
40
+
41
+ def unescape_stripe_callback_parameter(url)
42
+ url.gsub(ESCAPED_STRIPE_CALLBACK_PARAMETER, STRIPE_CALLBACK_PARAMETER)
43
+ end
44
+
45
+ # Default success URL. Override for custom behavior.
46
+ def callback_url(**)
47
+ unescape_stripe_callback_parameter url_for(
48
+ action: :show,
49
+ id: STRIPE_CALLBACK_PARAMETER,
50
+ only_path: false,
51
+ **
52
+ )
53
+ end
54
+ alias :success_url :callback_url
55
+ alias :cancel_url :callback_url
56
+ end
57
+ end
@@ -1,99 +1,5 @@
1
1
  module NoCheckout::Stripe
2
2
  class CheckoutSessionsController < ApplicationController
3
- def new
4
- redirect_to checkout_session.url, allow_other_host: true
5
- end
6
-
7
- protected
8
- def checkout_session
9
- @checkout_session ||= find_or_create_checkout_session
10
- end
11
-
12
- def create_checkout_session
13
- raise "Implement a method here that returns a Stripe::Checkout::Session"
14
- end
15
-
16
- def customer_id
17
- current_user.id
18
- end
19
-
20
- # Actually creates a Stripe checkout session. The reason I had to create
21
- # this method is so I could "curry" the values within so the `create_checkout_session`
22
- # could be a bit more readable and work better with inheritance.
23
- def create_stripe_checkout_session(**attributes)
24
- Stripe::Checkout::Session.create \
25
- mode: "subscription",
26
- customer: stripe_customer,
27
- success_url: success_url,
28
- cancel_url: cancel_url,
29
- **attributes
30
- end
31
-
32
- def callback_url(**kwargs)
33
- # Yuck! I have to do the append at the end because rails params escape the `{CHECKOUT_SESSION_ID}` values
34
- # to `session_id=%7BCHECKOUT_SESSION_ID%7D`. This will work though, but its def not pretty and feels a tad
35
- # dangerous.
36
- concat_unescaped_stripe_checkout_session_id url_for(action: :show, only_path: false, **kwargs)
37
- end
38
-
39
- STRIPE_CALLBACK_PARAMETER = "checkout_session_id={CHECKOUT_SESSION_ID}"
40
-
41
- # For some reason Stripe decided to not escape the `{CHECKOUT_SESSION_ID}`, if we try to
42
- # pass it through Rails URL builders or the URI object, it will URL encode the value and
43
- # not work with stripe. Consequently, we have to do some weirdness here to append the callback.
44
- #
45
- # More information at https://stripe.com/docs/payments/checkout/custom-success-page#modify-success-url
46
- def concat_unescaped_stripe_checkout_session_id(url)
47
- if URI(url).query
48
- url.concat("&#{STRIPE_CALLBACK_PARAMETER}")
49
- else
50
- url.concat("?#{STRIPE_CALLBACK_PARAMETER}")
51
- end
52
- end
53
-
54
- def success_url
55
- callback_url(state: :success)
56
- end
57
-
58
- def cancel_url
59
- callback_url(state: :cancel)
60
- end
61
-
62
- def stripe_customer
63
- @stripe_customer ||= find_or_create_customer
64
- end
65
-
66
- def create_customer
67
- Stripe::Customer.create(
68
- id: String(customer_id),
69
- name: current_user.name,
70
- email: current_user.email
71
- )
72
- end
73
-
74
- def find_or_create_checkout_session
75
- if params.key? :checkout_session_id
76
- Stripe::Checkout::Session.retrieve params.fetch(:checkout_session_id)
77
- else
78
- create_checkout_session
79
- end
80
- end
81
-
82
- def find_or_create_customer
83
- return nil if customer_id.blank?
84
-
85
- begin
86
- Stripe::Customer.retrieve(String(customer_id))
87
- # Blurg ... wish Stripe just returned a response object that's not an exception.
88
- rescue Stripe::InvalidRequestError => e
89
- case e.response.data
90
- in error: { code: "resource_missing" }
91
- create_customer
92
- else
93
- raise
94
- end
95
- end
96
- end
97
-
3
+ include CheckoutSession
98
4
  end
99
5
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NoCheckout
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nocheckout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-09-13 00:00:00.000000000 Z
10
+ date: 2025-10-24 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: zeitwerk
@@ -42,14 +41,14 @@ dependencies:
42
41
  name: rails
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
- - - "~>"
44
+ - - ">="
46
45
  - !ruby/object:Gem::Version
47
46
  version: '7.0'
48
47
  type: :runtime
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
- - - "~>"
51
+ - - ">="
53
52
  - !ruby/object:Gem::Version
54
53
  version: '7.0'
55
54
  description: Rails controllers for Stripe Checkout Sessions and Webhooks
@@ -69,6 +68,7 @@ files:
69
68
  - README.md
70
69
  - Rakefile
71
70
  - app/controllers/nocheckout/stripe.rb
71
+ - app/controllers/nocheckout/stripe/checkout_session.rb
72
72
  - app/controllers/nocheckout/stripe/checkout_sessions_controller.rb
73
73
  - app/controllers/nocheckout/stripe/webhooks_controller.rb
74
74
  - app/controllers/nocheckout/webhooks_controller.rb
@@ -87,7 +87,6 @@ metadata:
87
87
  homepage_uri: https://github.com/rubymonolith/nocheckout
88
88
  source_code_uri: https://github.com/rubymonolith/nocheckout
89
89
  changelog_uri: https://github.com/rubymonolith/nocheckout
90
- post_install_message:
91
90
  rdoc_options: []
92
91
  require_paths:
93
92
  - lib
@@ -102,8 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
101
  - !ruby/object:Gem::Version
103
102
  version: '0'
104
103
  requirements: []
105
- rubygems_version: 3.4.6
106
- signing_key:
104
+ rubygems_version: 3.6.2
107
105
  specification_version: 4
108
106
  summary: Rails controllers for Stripe Checkout Sessions and Webhooks
109
107
  test_files: []