shopify_app 19.0.2 → 19.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d047c2c86697b849fa77f14bf2d264f752d25e8756254f2bd87f1f788b1a71e4
4
- data.tar.gz: c8f75825dbfefb68fb068d21b794fe6b1f7a07fe43c6a875810fe9f9207ca970
3
+ metadata.gz: 0eed46621f78732a98d006f66631afbf2d5dcd17ed88af699ac20ff141df6ff6
4
+ data.tar.gz: 38180008e68e107e260eb0d629035ec65a8bd23915f361a4e9716ae4abca6604
5
5
  SHA512:
6
- metadata.gz: ccca162565545b8edf66dd3dc336df29b75f54020ade68f20f691aaf6c583d473a5c0b2af83cfd45f3ddfcdeee06c923ba82b023792f0865dc99ea721930130a
7
- data.tar.gz: e7ba37bf6451e51bf9a4d848b0466808c2c89caca5581f9685fa1e14c6b0c06b8c9cfa0f4fd4631b10d7d13efd056592a73eb02e822b69679f7b752fad404ec9
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.2)
4
+ shopify_app (19.1.0)
5
5
  activeresource
6
- browser_sniffer (~> 1.4.0)
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 (1.4.0)
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.3.0)
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.11)
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.3)
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.9944)
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: "/auth/shopify/callback",
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 "login" => :new, :as => :login
6
- post "login" => :create, :as => :authenticate
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 "auth/shopify/callback" => :callback
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
- * 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
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
- * Remove `allow_jwt_authentication=` and `allow_cookie_authentication=` invocations from
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
- * `v19.0.0` updates the `shopify_api` dependency to `10.0.0`. This version of `shopify_api` has breaking changes. See
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 `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.
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
- - *REMOVE* `config.per_user_tokens = [true|false]` this is no longer needed
126
- - *CHANGE* `config.session_repository = 'Shop'` To `config.shop_session_repository = 'Shop'`
127
- - *ADD (optional)* User Session Storage `config.user_session_repository = 'User'`
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
- - *CHANGE* `include ShopifyApp::SessionStorage` to `include ShopifyApp::ShopSessionStorage`
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
- - *CHANGE* if you are using shop sessions, `@shop_session` will need to be changed to `@current_shopify_session`.
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
- - *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.
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. The default implementation for this is.
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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyApp
4
- VERSION = "19.0.2"
4
+ VERSION = "19.1.0"
5
5
  end
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "19.0.1",
3
+ "version": "19.1.0",
4
4
  "repository": "git@github.com:Shopify/shopify_app.git",
5
5
  "author": "Shopify",
6
6
  "license": "MIT",
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", "~> 1.4.0")
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.2
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-04-27 00:00:00.000000000 Z
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: 1.4.0
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: 1.4.0
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.2.20
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