shopify_app 19.0.2 → 19.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 +4 -4
- data/CHANGELOG.md +10 -3
- data/Gemfile.lock +7 -7
- data/app/controllers/concerns/shopify_app/authenticated.rb +3 -0
- data/app/controllers/shopify_app/callback_controller.rb +3 -1
- data/app/controllers/shopify_app/sessions_controller.rb +3 -1
- data/config/routes.rb +17 -3
- data/docs/Upgrading.md +29 -16
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +13 -0
- data/lib/shopify_app/configuration.rb +31 -0
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +254 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +20 -0
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app.rb +1 -0
- data/package.json +1 -1
- data/shopify_app.gemspec +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0eed46621f78732a98d006f66631afbf2d5dcd17ed88af699ac20ff141df6ff6
|
4
|
+
data.tar.gz: 38180008e68e107e260eb0d629035ec65a8bd23915f361a4e9716ae4abca6604
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f0d7de43a22c940aa6e64ad343605b5ab4662d3cb9a9a4f21df3ba96f0fcee301f7430efed97dfcc55fe7b2b13dd7398c294895c08cbf777fb2b7b1fdce8fdf
|
7
|
+
data.tar.gz: 4671f6646686de7feb6c5b410ee435108fe98686bf35b6884e2305895d51038dbb7b24ac21d1da12b8e16dc71d831e6fbf769c5de510413b17d2c68cdfeb26ed
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
Unreleased
|
2
2
|
----------
|
3
3
|
|
4
|
+
19.1.0 (June 20, 2022)
|
5
|
+
----------
|
6
|
+
|
7
|
+
* Add the `login_callback_url` config to allow overwriting that route as well, and mount the engine routes based on the configurations. [#1445](https://github.com/Shopify/shopify_app/pull/1445)
|
8
|
+
* Add special headers when returning 401s from LoginProtection. [#1450](https://github.com/Shopify/shopify_app/pull/1450)
|
9
|
+
* Add a new `billing` configuration which takes in a `ShopifyApp::BillingConfiguration` object and checks for payment on controllers with `Authenticated`. [#1455](https://github.com/Shopify/shopify_app/pull/1455)
|
10
|
+
|
4
11
|
19.0.2 (April 27, 2022)
|
5
12
|
----------
|
6
13
|
|
@@ -27,7 +34,7 @@ BREAKING, please see migration notes.
|
|
27
34
|
18.1.0 (Jan 28, 2022)
|
28
35
|
----------
|
29
36
|
* Support Rails 7 [#1354](https://github.com/Shopify/shopify_app/pull/1354)
|
30
|
-
* Fix webhooks handling in Ruby 3 [#1342](https://github.com/Shopify/shopify_app/pull/1342)
|
37
|
+
* Fix webhooks handling in Ruby 3 [#1342](https://github.com/Shopify/shopify_app/pull/1342)
|
31
38
|
* Update to Ruby 3 and drop support to Ruby 2.5 [#1359](https://github.com/Shopify/shopify_app/pull/1359)
|
32
39
|
|
33
40
|
18.0.4 (Jan 27, 2022)
|
@@ -51,7 +58,7 @@ BREAKING, please see migration notes.
|
|
51
58
|
|
52
59
|
18.0.0 (May 3, 2021)
|
53
60
|
----------
|
54
|
-
* Support OmniAuth 2.x
|
61
|
+
* Support OmniAuth 2.x
|
55
62
|
* If your app has custom OmniAuth configuration, please refer to the [OmniAuth 2.0 upgrade guide](https://github.com/omniauth/omniauth/wiki/Upgrading-to-2.0).
|
56
63
|
* Support App Bridge version 2.x in the Embedded App layout. [#1241](https://github.com/Shopify/shopify_app/pull/1241)
|
57
64
|
|
@@ -81,7 +88,7 @@ BREAKING, please see migration notes.
|
|
81
88
|
|
82
89
|
17.0.4 (January 25, 2021)
|
83
90
|
----------
|
84
|
-
* Redirect user to login page if shopify domain is not found in the `EnsureAuthenticatedLinks` concern [#1158](https://github.com/Shopify/shopify_app/pull/1158)
|
91
|
+
* Redirect user to login page if shopify domain is not found in the `EnsureAuthenticatedLinks` concern [#1158](https://github.com/Shopify/shopify_app/pull/1158)
|
85
92
|
|
86
93
|
17.0.3 (January 22, 2021)
|
87
94
|
----------
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
shopify_app (19.0
|
4
|
+
shopify_app (19.1.0)
|
5
5
|
activeresource
|
6
|
-
browser_sniffer (~>
|
6
|
+
browser_sniffer (~> 2.0)
|
7
7
|
jwt (>= 2.2.3)
|
8
8
|
rails (> 5.2.1)
|
9
9
|
redirect_safely (~> 1.0)
|
@@ -85,7 +85,7 @@ GEM
|
|
85
85
|
ast (2.4.2)
|
86
86
|
binding_of_caller (1.0.0)
|
87
87
|
debug_inspector (>= 0.0.1)
|
88
|
-
browser_sniffer (
|
88
|
+
browser_sniffer (2.0.0)
|
89
89
|
builder (3.2.4)
|
90
90
|
byebug (11.1.3)
|
91
91
|
coderay (1.1.3)
|
@@ -104,7 +104,7 @@ GEM
|
|
104
104
|
multi_xml (>= 0.5.2)
|
105
105
|
i18n (1.10.0)
|
106
106
|
concurrent-ruby (~> 1.0)
|
107
|
-
jwt (2.
|
107
|
+
jwt (2.4.1)
|
108
108
|
loofah (2.15.0)
|
109
109
|
crass (~> 1.0.2)
|
110
110
|
nokogiri (>= 1.5.9)
|
@@ -124,7 +124,7 @@ GEM
|
|
124
124
|
nokogiri (1.13.4)
|
125
125
|
mini_portile2 (~> 2.8.0)
|
126
126
|
racc (~> 1.4)
|
127
|
-
oj (3.13.
|
127
|
+
oj (3.13.14)
|
128
128
|
openssl (3.0.0)
|
129
129
|
parallel (1.21.0)
|
130
130
|
parser (3.1.0.0)
|
@@ -194,7 +194,7 @@ GEM
|
|
194
194
|
rubocop (~> 1.24)
|
195
195
|
ruby-progressbar (1.11.0)
|
196
196
|
securerandom (0.2.0)
|
197
|
-
shopify_api (10.0
|
197
|
+
shopify_api (10.1.0)
|
198
198
|
concurrent-ruby
|
199
199
|
hash_diff
|
200
200
|
httparty
|
@@ -204,7 +204,7 @@ GEM
|
|
204
204
|
securerandom
|
205
205
|
sorbet-runtime
|
206
206
|
zeitwerk (~> 2.5)
|
207
|
-
sorbet-runtime (0.5.
|
207
|
+
sorbet-runtime (0.5.10101)
|
208
208
|
sprockets (4.0.3)
|
209
209
|
concurrent-ruby (~> 1.0)
|
210
210
|
rack (> 1, < 3)
|
@@ -9,8 +9,11 @@ module ShopifyApp
|
|
9
9
|
include ShopifyApp::LoginProtection
|
10
10
|
include ShopifyApp::CsrfProtection
|
11
11
|
include ShopifyApp::EmbeddedApp
|
12
|
+
include ShopifyApp::EnsureBilling
|
13
|
+
|
12
14
|
before_action :login_again_if_different_user_or_shop
|
13
15
|
around_action :activate_shopify_session
|
16
|
+
after_action :add_top_level_redirection_headers
|
14
17
|
end
|
15
18
|
end
|
16
19
|
end
|
@@ -4,6 +4,7 @@ module ShopifyApp
|
|
4
4
|
# Performs login after OAuth completes
|
5
5
|
class CallbackController < ActionController::Base
|
6
6
|
include ShopifyApp::LoginProtection
|
7
|
+
include ShopifyApp::EnsureBilling
|
7
8
|
|
8
9
|
def callback
|
9
10
|
begin
|
@@ -34,8 +35,9 @@ module ShopifyApp
|
|
34
35
|
end
|
35
36
|
|
36
37
|
perform_post_authenticate_jobs(auth_result[:session])
|
38
|
+
has_payment = check_billing(auth_result[:session])
|
37
39
|
|
38
|
-
respond_successfully
|
40
|
+
respond_successfully if has_payment
|
39
41
|
end
|
40
42
|
|
41
43
|
private
|
@@ -44,9 +44,11 @@ module ShopifyApp
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def start_oauth
|
47
|
+
callback_url = ShopifyApp.configuration.login_callback_url.gsub(%r{^/}, "")
|
48
|
+
|
47
49
|
auth_attributes = ShopifyAPI::Auth::Oauth.begin_auth(
|
48
50
|
shop: sanitized_shop_name,
|
49
|
-
redirect_path: "
|
51
|
+
redirect_path: "/#{callback_url}",
|
50
52
|
is_online: user_session_expected?
|
51
53
|
)
|
52
54
|
cookies.encrypted[auth_attributes[:cookie].name] = {
|
data/config/routes.rb
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
ShopifyApp::Engine.routes.draw do
|
4
|
+
login_url = ShopifyApp.configuration.login_url.gsub(/^#{ShopifyApp.configuration.root_url}/, "")
|
5
|
+
login_callback_url = ShopifyApp.configuration.login_callback_url.gsub(/^#{ShopifyApp.configuration.root_url}/, "")
|
6
|
+
|
4
7
|
controller :sessions do
|
5
|
-
get
|
6
|
-
post
|
8
|
+
get login_url => :new, :as => :login
|
9
|
+
post login_url => :create, :as => :authenticate
|
7
10
|
get "logout" => :destroy, :as => :logout
|
11
|
+
|
12
|
+
# Kept to prevent apps relying on these routes from breaking
|
13
|
+
if login_url.gsub(%r{^/}, "") != "login"
|
14
|
+
get "login" => :new, :as => :default_login
|
15
|
+
post "login" => :create, :as => :default_authenticate
|
16
|
+
end
|
8
17
|
end
|
9
18
|
|
10
19
|
controller :callback do
|
11
|
-
get
|
20
|
+
get login_callback_url => :callback
|
21
|
+
|
22
|
+
# Kept to prevent apps relying on these routes from breaking
|
23
|
+
if login_callback_url.gsub(%r{^/}, "") != "auth/shopify/callback"
|
24
|
+
get "auth/shopify/callback" => :default_callback
|
25
|
+
end
|
12
26
|
end
|
13
27
|
|
14
28
|
namespace :webhooks do
|
data/docs/Upgrading.md
CHANGED
@@ -23,18 +23,23 @@ gem.
|
|
23
23
|
|
24
24
|
### High-level process
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
- Delete `config/initializers/omniauth.rb` as apps no longer need to initialize `OmniAuth` directly.
|
27
|
+
- Delete `config/initializers/user_agent.rb` as `shopify_app` will set the right `User-Agent` header for interacting
|
28
28
|
with the Shopify API. If the app requires further information in the `User-Agent` header beyond what Shopify API
|
29
29
|
requires, specify this in the `ShopifyAPI::Context.user_agent_prefix` setting.
|
30
|
-
|
30
|
+
- Remove `allow_jwt_authentication=` and `allow_cookie_authentication=` invocations from
|
31
31
|
`config/initializers/shopify_app.rb` as the decision logic for which authentication method to use is now handled
|
32
32
|
internally by the `shopify_api` gem, using the `ShopifyAPI::Context.embedded_app` setting.
|
33
|
-
|
33
|
+
- `v19.0.0` updates the `shopify_api` dependency to `10.0.0`. This version of `shopify_api` has breaking changes. See
|
34
34
|
the documentation for addressing these breaking changes on GitHub [here](https://github.com/Shopify/shopify_api#breaking-change-notice-for-version-1000).
|
35
35
|
|
36
36
|
### Specific cases
|
37
37
|
|
38
|
+
#### Shopify user id in session
|
39
|
+
|
40
|
+
Previously, we set the entire app user object in the `session` object.
|
41
|
+
As of v19, since we no longer save the app user to the session (but only the shopify user id), we now store it as `session[:shopify_user_id]`. Please make sure to update any references to that object.
|
42
|
+
|
38
43
|
#### Webhook Jobs
|
39
44
|
|
40
45
|
Add a new `handle` method to existing webhook jobs to go through the updated `shopify_api` gem.
|
@@ -97,6 +102,7 @@ Rails.application.config.after_initialize do
|
|
97
102
|
end
|
98
103
|
end
|
99
104
|
```
|
105
|
+
|
100
106
|
## Upgrading to `v18.1.2`
|
101
107
|
|
102
108
|
Version 18.1.2 replaces the deprecated EASDK redirect with an App Bridge 2 redirect when attempting to break out of an iframe. This happens when an app is installed, requires new access scopes, or re-authentication because the login session is expired.
|
@@ -105,7 +111,7 @@ Version 18.1.2 replaces the deprecated EASDK redirect with an App Bridge 2 redir
|
|
105
111
|
|
106
112
|
### Different SameSite cookie attribute behaviour
|
107
113
|
|
108
|
-
To support Rails
|
114
|
+
To support Rails `v6.1`, the [`SameSiteCookieMiddleware`](/lib/shopify_app/middleware/same_site_cookie_middleware.rb) was updated to configure cookies to `SameSite=None` if the app is embedded. Before this release, cookies were configured to `SameSite=None` only if this attribute had not previously been set before.
|
109
115
|
|
110
116
|
```diff
|
111
117
|
# same_site_cookie_middleware.rb
|
@@ -122,32 +128,33 @@ change to how session stores work. Here are the steps to migrate to 13.x
|
|
122
128
|
|
123
129
|
### Changes to `config/initializers/shopify_app.rb`
|
124
130
|
|
125
|
-
-
|
126
|
-
-
|
127
|
-
-
|
131
|
+
- _REMOVE_ `config.per_user_tokens = [true|false]` this is no longer needed
|
132
|
+
- _CHANGE_ `config.session_repository = 'Shop'` To `config.shop_session_repository = 'Shop'`
|
133
|
+
- _ADD (optional)_ User Session Storage `config.user_session_repository = 'User'`
|
128
134
|
|
129
135
|
### Shop Model Changes (normally `app/models/shop.rb`)
|
130
136
|
|
131
|
-
-
|
137
|
+
- _CHANGE_ `include ShopifyApp::SessionStorage` to `include ShopifyApp::ShopSessionStorage`
|
132
138
|
|
133
139
|
### Changes to the @shop_session instance variable (normally in `app/controllers/*.rb`)
|
134
140
|
|
135
|
-
-
|
141
|
+
- _CHANGE_ if you are using shop sessions, `@shop_session` will need to be changed to `@current_shopify_session`.
|
136
142
|
|
137
143
|
### Changes to Rails `session`
|
138
144
|
|
139
|
-
-
|
145
|
+
- _CHANGE_ `session[:shopify]` is no longer set. Use `session[:user_id]` if your app uses user based tokens, or `session[:shop_id]` if your app uses shop based tokens.
|
140
146
|
|
141
147
|
### Changes to `ShopifyApp::LoginProtection`
|
142
148
|
|
143
149
|
`ShopifyApp::LoginProtection`
|
144
150
|
|
145
151
|
- CHANGE if you are using `ShopifyApp::LoginProtection#shopify_session` in your code, it will need to be
|
146
|
-
changed to `ShopifyApp::LoginProtection#activate_shopify_session`
|
152
|
+
changed to `ShopifyApp::LoginProtection#activate_shopify_session`
|
147
153
|
- CHANGE if you are using `ShopifyApp::LoginProtection#clear_shop_session` in your code, it will need to be
|
148
|
-
changed to `ShopifyApp::LoginProtection#clear_shopify_session`
|
154
|
+
changed to `ShopifyApp::LoginProtection#clear_shopify_session`
|
149
155
|
|
150
156
|
### Notes
|
157
|
+
|
151
158
|
You do not need a user model; a shop session is fine for most applications.
|
152
159
|
|
153
160
|
---
|
@@ -155,6 +162,7 @@ You do not need a user model; a shop session is fine for most applications.
|
|
155
162
|
## Upgrading to `v11.7.0`
|
156
163
|
|
157
164
|
### Session storage method signature breaking change
|
165
|
+
|
158
166
|
If you override `def self.store(auth_session)` method in your session storage model (e.g. Shop), the method signature has changed to `def self.store(auth_session, *args)` in order to support user-based token storage. Please update your method signature to include the second argument.
|
159
167
|
|
160
168
|
---
|
@@ -165,13 +173,15 @@ If you override `def self.store(auth_session)` method in your session storage mo
|
|
165
173
|
|
166
174
|
Add an API version configuration in `config/initializers/shopify_app.rb`
|
167
175
|
Set this to the version you want to run against by default. See [Shopify API docs](https://help.shopify.com/api/versioning) for versions available.
|
176
|
+
|
168
177
|
```ruby
|
169
178
|
config.api_version = '2019-04'
|
170
179
|
```
|
171
180
|
|
172
181
|
### Session storage change
|
173
182
|
|
174
|
-
You will need to add an `api_version` method to your session storage object.
|
183
|
+
You will need to add an `api_version` method to your session storage object. The default implementation for this is.
|
184
|
+
|
175
185
|
```ruby
|
176
186
|
def api_version
|
177
187
|
ShopifyApp.configuration.api_version
|
@@ -181,6 +191,7 @@ end
|
|
181
191
|
### Generated file change
|
182
192
|
|
183
193
|
`embedded_app.html.erb` the usage of `shop_session.url` needs to be changed to `shop_session.domain`
|
194
|
+
|
184
195
|
```erb
|
185
196
|
<script type="text/javascript">
|
186
197
|
ShopifyApp.init({
|
@@ -193,7 +204,9 @@ end
|
|
193
204
|
});
|
194
205
|
</script>
|
195
206
|
```
|
207
|
+
|
196
208
|
is changed to
|
209
|
+
|
197
210
|
```erb
|
198
211
|
<script type="text/javascript">
|
199
212
|
ShopifyApp.init({
|
@@ -211,5 +224,5 @@ is changed to
|
|
211
224
|
|
212
225
|
You will need to also follow the ShopifyAPI [upgrade guide](https://github.com/Shopify/shopify_api/blob/master/README.md#-breaking-change-notice-for-version-700-) to ensure your app is ready to work with API versioning.
|
213
226
|
|
214
|
-
[dashboard]:https://partners.shopify.com
|
215
|
-
[app-bridge]:https://shopify.dev/apps/tools/app-bridge
|
227
|
+
[dashboard]: https://partners.shopify.com
|
228
|
+
[app-bridge]: https://shopify.dev/apps/tools/app-bridge
|
@@ -13,6 +13,19 @@ ShopifyApp.configure do |config|
|
|
13
13
|
config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence
|
14
14
|
config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence
|
15
15
|
|
16
|
+
# You may want to charge merchants for using your app. Setting the billing configuration will cause the Authenticated
|
17
|
+
# controller concern to check that the session is for a merchant that has an active one-time payment or subscription.
|
18
|
+
# If no payment is found, it starts off the process and sends the merchant to a confirmation URL so that they can
|
19
|
+
# approve the purchase.
|
20
|
+
#
|
21
|
+
# Learn more about billing in our documentation: https://shopify.dev/apps/billing
|
22
|
+
# config.billing = ShopifyApp::BillingConfiguration.new(
|
23
|
+
# charge_name: "My app billing charge",
|
24
|
+
# amount: 5,
|
25
|
+
# interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS,
|
26
|
+
# currency_code: "USD", # Only supports USD for now
|
27
|
+
# )
|
28
|
+
|
16
29
|
if defined? Rails::Server
|
17
30
|
raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#requirements') unless config.api_key
|
18
31
|
raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#requirements') unless config.secret
|
@@ -24,6 +24,7 @@ module ShopifyApp
|
|
24
24
|
# customise urls
|
25
25
|
attr_accessor :root_url
|
26
26
|
attr_writer :login_url
|
27
|
+
attr_writer :login_callback_url
|
27
28
|
|
28
29
|
# customise ActiveJob queue names
|
29
30
|
attr_accessor :scripttags_manager_queue_name
|
@@ -38,6 +39,9 @@ module ShopifyApp
|
|
38
39
|
# allow namespacing webhook jobs
|
39
40
|
attr_accessor :webhook_jobs_namespace
|
40
41
|
|
42
|
+
# takes a ShopifyApp::BillingConfiguration object
|
43
|
+
attr_accessor :billing
|
44
|
+
|
41
45
|
def initialize
|
42
46
|
@root_url = "/"
|
43
47
|
@myshopify_domain = "myshopify.com"
|
@@ -50,6 +54,11 @@ module ShopifyApp
|
|
50
54
|
@login_url || File.join(@root_url, "login")
|
51
55
|
end
|
52
56
|
|
57
|
+
def login_callback_url
|
58
|
+
# Not including @root_url to keep historic behaviour
|
59
|
+
@login_callback_url || File.join("auth/shopify/callback")
|
60
|
+
end
|
61
|
+
|
53
62
|
def user_session_repository=(klass)
|
54
63
|
ShopifyApp::SessionRepository.user_storage = klass
|
55
64
|
end
|
@@ -84,6 +93,10 @@ module ShopifyApp
|
|
84
93
|
scripttags.present?
|
85
94
|
end
|
86
95
|
|
96
|
+
def requires_billing?
|
97
|
+
billing.present?
|
98
|
+
end
|
99
|
+
|
87
100
|
def shop_access_scopes
|
88
101
|
@shop_access_scopes || scope
|
89
102
|
end
|
@@ -93,6 +106,24 @@ module ShopifyApp
|
|
93
106
|
end
|
94
107
|
end
|
95
108
|
|
109
|
+
class BillingConfiguration
|
110
|
+
INTERVAL_ONE_TIME = "ONE_TIME"
|
111
|
+
INTERVAL_EVERY_30_DAYS = "EVERY_30_DAYS"
|
112
|
+
INTERVAL_ANNUAL = "ANNUAL"
|
113
|
+
|
114
|
+
attr_reader :charge_name
|
115
|
+
attr_reader :amount
|
116
|
+
attr_reader :currency_code
|
117
|
+
attr_reader :interval
|
118
|
+
|
119
|
+
def initialize(charge_name:, amount:, interval:, currency_code: "USD")
|
120
|
+
@charge_name = charge_name
|
121
|
+
@amount = amount
|
122
|
+
@currency_code = currency_code
|
123
|
+
@interval = interval
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
96
127
|
def self.configuration
|
97
128
|
@configuration ||= Configuration.new
|
98
129
|
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module EnsureBilling
|
5
|
+
class BillingError < StandardError
|
6
|
+
attr_accessor :message
|
7
|
+
attr_accessor :errors
|
8
|
+
|
9
|
+
def initialize(message, errors)
|
10
|
+
super
|
11
|
+
@message = message
|
12
|
+
@errors = errors
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
RECURRING_INTERVALS = [BillingConfiguration::INTERVAL_EVERY_30_DAYS, BillingConfiguration::INTERVAL_ANNUAL]
|
19
|
+
|
20
|
+
included do
|
21
|
+
before_action :check_billing, if: :billing_required?
|
22
|
+
rescue_from BillingError, with: :handle_billing_error
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def check_billing(session = current_shopify_session)
|
28
|
+
return true if session.blank? || !billing_required?
|
29
|
+
|
30
|
+
confirmation_url = nil
|
31
|
+
|
32
|
+
if has_active_payment?(session)
|
33
|
+
has_payment = true
|
34
|
+
else
|
35
|
+
has_payment = false
|
36
|
+
confirmation_url = request_payment(session)
|
37
|
+
end
|
38
|
+
|
39
|
+
unless has_payment
|
40
|
+
if request.xhr?
|
41
|
+
add_top_level_redirection_headers(url: confirmation_url, ignore_response_code: true)
|
42
|
+
head(:unauthorized)
|
43
|
+
else
|
44
|
+
redirect_to(confirmation_url, allow_other_host: true)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
has_payment
|
49
|
+
end
|
50
|
+
|
51
|
+
def billing_required?
|
52
|
+
ShopifyApp.configuration.requires_billing?
|
53
|
+
end
|
54
|
+
|
55
|
+
def handle_billing_error(error)
|
56
|
+
logger.info("#{error.message}: #{error.errors}")
|
57
|
+
redirect_to_login
|
58
|
+
end
|
59
|
+
|
60
|
+
def has_active_payment?(session)
|
61
|
+
if recurring?
|
62
|
+
has_subscription?(session)
|
63
|
+
else
|
64
|
+
has_one_time_payment?(session)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def has_subscription?(session)
|
69
|
+
response = run_query(session: session, query: RECURRING_PURCHASES_QUERY)
|
70
|
+
subscriptions = response.body["data"]["currentAppInstallation"]["activeSubscriptions"]
|
71
|
+
|
72
|
+
subscriptions.each do |subscription|
|
73
|
+
if subscription["name"] == ShopifyApp.configuration.billing.charge_name &&
|
74
|
+
(!Rails.env.production? || !subscription["test"])
|
75
|
+
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
def has_one_time_payment?(session)
|
84
|
+
purchases = nil
|
85
|
+
end_cursor = nil
|
86
|
+
|
87
|
+
loop do
|
88
|
+
response = run_query(session: session, query: ONE_TIME_PURCHASES_QUERY, variables: { endCursor: end_cursor })
|
89
|
+
purchases = response.body["data"]["currentAppInstallation"]["oneTimePurchases"]
|
90
|
+
|
91
|
+
purchases["edges"].each do |purchase|
|
92
|
+
node = purchase["node"]
|
93
|
+
|
94
|
+
if node["name"] == ShopifyApp.configuration.billing.charge_name &&
|
95
|
+
(!Rails.env.production? || !node["test"]) &&
|
96
|
+
node["status"] == "ACTIVE"
|
97
|
+
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end_cursor = purchases["pageInfo"]["endCursor"]
|
103
|
+
break unless purchases["pageInfo"]["hasNextPage"]
|
104
|
+
end
|
105
|
+
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
def request_payment(session)
|
110
|
+
shop = session.shop
|
111
|
+
host = Base64.encode64("#{shop}/admin")
|
112
|
+
return_url = "https://#{ShopifyAPI::Context.host_name}?shop=#{shop}&host=#{host}"
|
113
|
+
|
114
|
+
if recurring?
|
115
|
+
data = request_recurring_payment(session: session, return_url: return_url)
|
116
|
+
data = data["data"]["appSubscriptionCreate"]
|
117
|
+
else
|
118
|
+
data = request_one_time_payment(session: session, return_url: return_url)
|
119
|
+
data = data["data"]["appPurchaseOneTimeCreate"]
|
120
|
+
end
|
121
|
+
|
122
|
+
raise BillingError.new("Error while billing the store", data["userErrros"]) unless data["userErrors"].empty?
|
123
|
+
|
124
|
+
data["confirmationUrl"]
|
125
|
+
end
|
126
|
+
|
127
|
+
def request_recurring_payment(session:, return_url:)
|
128
|
+
response = run_query(
|
129
|
+
session: session,
|
130
|
+
query: RECURRING_PURCHASE_MUTATION,
|
131
|
+
variables: {
|
132
|
+
name: ShopifyApp.configuration.billing.charge_name,
|
133
|
+
lineItems: {
|
134
|
+
plan: {
|
135
|
+
appRecurringPricingDetails: {
|
136
|
+
interval: ShopifyApp.configuration.billing.interval,
|
137
|
+
price: {
|
138
|
+
amount: ShopifyApp.configuration.billing.amount,
|
139
|
+
currencyCode: ShopifyApp.configuration.billing.currency_code,
|
140
|
+
},
|
141
|
+
},
|
142
|
+
},
|
143
|
+
},
|
144
|
+
returnUrl: return_url,
|
145
|
+
test: !Rails.env.production?,
|
146
|
+
}
|
147
|
+
)
|
148
|
+
|
149
|
+
response.body
|
150
|
+
end
|
151
|
+
|
152
|
+
def request_one_time_payment(session:, return_url:)
|
153
|
+
response = run_query(
|
154
|
+
session: session,
|
155
|
+
query: ONE_TIME_PURCHASE_MUTATION,
|
156
|
+
variables: {
|
157
|
+
name: ShopifyApp.configuration.billing.charge_name,
|
158
|
+
price: {
|
159
|
+
amount: ShopifyApp.configuration.billing.amount,
|
160
|
+
currencyCode: ShopifyApp.configuration.billing.currency_code,
|
161
|
+
},
|
162
|
+
returnUrl: return_url,
|
163
|
+
test: !Rails.env.production?,
|
164
|
+
}
|
165
|
+
)
|
166
|
+
|
167
|
+
response.body
|
168
|
+
end
|
169
|
+
|
170
|
+
def recurring?
|
171
|
+
RECURRING_INTERVALS.include?(ShopifyApp.configuration.billing.interval)
|
172
|
+
end
|
173
|
+
|
174
|
+
def run_query(session:, query:, variables: nil)
|
175
|
+
client = ShopifyAPI::Clients::Graphql::Admin.new(session: session)
|
176
|
+
|
177
|
+
response = client.query(query: query, variables: variables)
|
178
|
+
|
179
|
+
raise BillingError.new("Error while billing the store", []) unless response.ok?
|
180
|
+
raise BillingError.new("Error while billing the store", response.body["errors"]) if response.body["errors"]
|
181
|
+
|
182
|
+
response
|
183
|
+
end
|
184
|
+
|
185
|
+
RECURRING_PURCHASES_QUERY = <<~'QUERY'
|
186
|
+
query appSubscription {
|
187
|
+
currentAppInstallation {
|
188
|
+
activeSubscriptions {
|
189
|
+
name, test
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
QUERY
|
194
|
+
|
195
|
+
ONE_TIME_PURCHASES_QUERY = <<~'QUERY'
|
196
|
+
query appPurchases($endCursor: String) {
|
197
|
+
currentAppInstallation {
|
198
|
+
oneTimePurchases(first: 250, sortKey: CREATED_AT, after: $endCursor) {
|
199
|
+
edges {
|
200
|
+
node {
|
201
|
+
name, test, status
|
202
|
+
}
|
203
|
+
}
|
204
|
+
pageInfo {
|
205
|
+
hasNextPage, endCursor
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
209
|
+
}
|
210
|
+
QUERY
|
211
|
+
|
212
|
+
RECURRING_PURCHASE_MUTATION = <<~'QUERY'
|
213
|
+
mutation createPaymentMutation(
|
214
|
+
$name: String!
|
215
|
+
$lineItems: [AppSubscriptionLineItemInput!]!
|
216
|
+
$returnUrl: URL!
|
217
|
+
$test: Boolean
|
218
|
+
) {
|
219
|
+
appSubscriptionCreate(
|
220
|
+
name: $name
|
221
|
+
lineItems: $lineItems
|
222
|
+
returnUrl: $returnUrl
|
223
|
+
test: $test
|
224
|
+
) {
|
225
|
+
confirmationUrl
|
226
|
+
userErrors {
|
227
|
+
field, message
|
228
|
+
}
|
229
|
+
}
|
230
|
+
}
|
231
|
+
QUERY
|
232
|
+
|
233
|
+
ONE_TIME_PURCHASE_MUTATION = <<~'QUERY'
|
234
|
+
mutation createPaymentMutation(
|
235
|
+
$name: String!
|
236
|
+
$price: MoneyInput!
|
237
|
+
$returnUrl: URL!
|
238
|
+
$test: Boolean
|
239
|
+
) {
|
240
|
+
appPurchaseOneTimeCreate(
|
241
|
+
name: $name
|
242
|
+
price: $price
|
243
|
+
returnUrl: $returnUrl
|
244
|
+
test: $test
|
245
|
+
) {
|
246
|
+
confirmationUrl
|
247
|
+
userErrors {
|
248
|
+
field, message
|
249
|
+
}
|
250
|
+
}
|
251
|
+
}
|
252
|
+
QUERY
|
253
|
+
end
|
254
|
+
end
|
@@ -70,6 +70,25 @@ module ShopifyApp
|
|
70
70
|
expire_at - 5.seconds # 5s gap to start fetching new token in advance
|
71
71
|
end
|
72
72
|
|
73
|
+
def add_top_level_redirection_headers(url: nil, ignore_response_code: false)
|
74
|
+
if request.xhr? && (ignore_response_code || response.code.to_i == 401)
|
75
|
+
# Make sure the shop is set in the redirection URL
|
76
|
+
unless params[:shop]
|
77
|
+
params[:shop] = if current_shopify_session
|
78
|
+
current_shopify_session.shop
|
79
|
+
elsif (matches = request.headers["HTTP_AUTHORIZATION"]&.match(/^Bearer (.+)$/))
|
80
|
+
jwt_payload = ShopifyAPI::Auth::JwtPayload.new(T.must(matches[1]))
|
81
|
+
jwt_payload.shop
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
url ||= login_url_with_optional_shop
|
86
|
+
|
87
|
+
response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1")
|
88
|
+
response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
73
92
|
protected
|
74
93
|
|
75
94
|
def jwt_shopify_domain
|
@@ -86,6 +105,7 @@ module ShopifyApp
|
|
86
105
|
|
87
106
|
def redirect_to_login
|
88
107
|
if request.xhr?
|
108
|
+
add_top_level_redirection_headers(ignore_response_code: true)
|
89
109
|
head(:unauthorized)
|
90
110
|
else
|
91
111
|
if request.get?
|
data/lib/shopify_app/version.rb
CHANGED
data/lib/shopify_app.rb
CHANGED
@@ -39,6 +39,7 @@ module ShopifyApp
|
|
39
39
|
require "shopify_app/controller_concerns/localization"
|
40
40
|
require "shopify_app/controller_concerns/itp"
|
41
41
|
require "shopify_app/controller_concerns/login_protection"
|
42
|
+
require "shopify_app/controller_concerns/ensure_billing"
|
42
43
|
require "shopify_app/controller_concerns/embedded_app"
|
43
44
|
require "shopify_app/controller_concerns/payload_verification"
|
44
45
|
require "shopify_app/controller_concerns/app_proxy_verification"
|
data/package.json
CHANGED
data/shopify_app.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.metadata["allowed_push_host"] = "https://rubygems.org"
|
16
16
|
|
17
17
|
s.add_runtime_dependency("activeresource") # TODO: Remove this once all active resource dependencies are removed
|
18
|
-
s.add_runtime_dependency("browser_sniffer", "~>
|
18
|
+
s.add_runtime_dependency("browser_sniffer", "~> 2.0")
|
19
19
|
s.add_runtime_dependency("jwt", ">= 2.2.3")
|
20
20
|
s.add_runtime_dependency("rails", "> 5.2.1")
|
21
21
|
s.add_runtime_dependency("redirect_safely", "~> 1.0")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify_app
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 19.0
|
4
|
+
version: 19.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activeresource
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: '2.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: '2.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: jwt
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -399,6 +399,7 @@ files:
|
|
399
399
|
- lib/shopify_app/controller_concerns/app_proxy_verification.rb
|
400
400
|
- lib/shopify_app/controller_concerns/csrf_protection.rb
|
401
401
|
- lib/shopify_app/controller_concerns/embedded_app.rb
|
402
|
+
- lib/shopify_app/controller_concerns/ensure_billing.rb
|
402
403
|
- lib/shopify_app/controller_concerns/itp.rb
|
403
404
|
- lib/shopify_app/controller_concerns/localization.rb
|
404
405
|
- lib/shopify_app/controller_concerns/login_protection.rb
|
@@ -451,7 +452,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
451
452
|
- !ruby/object:Gem::Version
|
452
453
|
version: '0'
|
453
454
|
requirements: []
|
454
|
-
rubygems_version: 3.
|
455
|
+
rubygems_version: 3.3.3
|
455
456
|
signing_key:
|
456
457
|
specification_version: 4
|
457
458
|
summary: This gem is used to get quickly started with the Shopify API
|