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 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