shopify_app 19.0.1 → 20.0.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.
@@ -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?
@@ -232,7 +252,13 @@ module ShopifyApp
232
252
  current_shopify_session && params[:shop].is_a?(String) && current_shopify_session.shop != params[:shop]
233
253
  end
234
254
 
255
+ def shop_session
256
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(sanitize_shop_param(params))
257
+ end
258
+
235
259
  def user_session_expected?
260
+ return false if shop_session.nil?
261
+ return false if ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(shop_session.shop)
236
262
  !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
237
263
  end
238
264
  end
@@ -46,7 +46,7 @@ module ShopifyApp
46
46
  # ShopifyAPI::Auth::SessionStorage override
47
47
  def store_session(session)
48
48
  if session.online?
49
- user_storage.store(session, session.associated_user.id.to_s)
49
+ user_storage.store(session, session.associated_user)
50
50
  else
51
51
  shop_storage.store(session)
52
52
  end
@@ -11,7 +11,7 @@ module ShopifyApp
11
11
 
12
12
  class_methods do
13
13
  def store(auth_session, user)
14
- user = find_or_initialize_by(shopify_user_id: user[:id])
14
+ user = find_or_initialize_by(shopify_user_id: user.id)
15
15
  user.shopify_token = auth_session.access_token
16
16
  user.shopify_domain = auth_session.shop
17
17
  user.access_scopes = auth_session.scope.to_s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyApp
4
- VERSION = "19.0.1"
4
+ VERSION = "20.0.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": "20.0.0",
4
4
  "repository": "git@github.com:Shopify/shopify_app.git",
5
5
  "author": "Shopify",
6
6
  "license": "MIT",
data/shopify_app.gemspec CHANGED
@@ -10,16 +10,16 @@ Gem::Specification.new do |s|
10
10
  s.author = "Shopify"
11
11
  s.summary = "This gem is used to get quickly started with the Shopify API"
12
12
 
13
- s.required_ruby_version = ">= 2.6"
13
+ s.required_ruby_version = ">= 2.7"
14
14
 
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")
22
- s.add_runtime_dependency("shopify_api", "~> 10.0")
22
+ s.add_runtime_dependency("shopify_api", "~> 11.0")
23
23
  s.add_runtime_dependency("sprockets-rails", ">= 2.0.0")
24
24
 
25
25
  s.add_development_dependency("byebug")
data/yarn.lock CHANGED
@@ -3515,9 +3515,9 @@ minimatch@3.0.4, minimatch@^3.0.4:
3515
3515
  brace-expansion "^1.1.7"
3516
3516
 
3517
3517
  minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
3518
- version "1.2.5"
3519
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
3520
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
3518
+ version "1.2.6"
3519
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
3520
+ integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
3521
3521
 
3522
3522
  mississippi@^3.0.0:
3523
3523
  version "3.0.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.1
4
+ version: 20.0.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-11 00:00:00.000000000 Z
11
+ date: 2022-07-04 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
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '10.0'
89
+ version: '11.0'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '10.0'
96
+ version: '11.0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: sprockets-rails
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -277,8 +277,9 @@ files:
277
277
  - Rakefile
278
278
  - SECURITY.md
279
279
  - app/assets/images/storage_access.svg
280
- - app/assets/javascripts/shopify_app/app_bridge_2.0.12.js
280
+ - app/assets/javascripts/shopify_app/app_bridge_3.1.1.js
281
281
  - app/assets/javascripts/shopify_app/app_bridge_redirect.js
282
+ - app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js
282
283
  - app/assets/javascripts/shopify_app/enable_cookies.js
283
284
  - app/assets/javascripts/shopify_app/itp_helper.js
284
285
  - app/assets/javascripts/shopify_app/partition_cookies.js
@@ -399,6 +400,7 @@ files:
399
400
  - lib/shopify_app/controller_concerns/app_proxy_verification.rb
400
401
  - lib/shopify_app/controller_concerns/csrf_protection.rb
401
402
  - lib/shopify_app/controller_concerns/embedded_app.rb
403
+ - lib/shopify_app/controller_concerns/ensure_billing.rb
402
404
  - lib/shopify_app/controller_concerns/itp.rb
403
405
  - lib/shopify_app/controller_concerns/localization.rb
404
406
  - lib/shopify_app/controller_concerns/login_protection.rb
@@ -444,14 +446,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
444
446
  requirements:
445
447
  - - ">="
446
448
  - !ruby/object:Gem::Version
447
- version: '2.6'
449
+ version: '2.7'
448
450
  required_rubygems_version: !ruby/object:Gem::Requirement
449
451
  requirements:
450
452
  - - ">="
451
453
  - !ruby/object:Gem::Version
452
454
  version: '0'
453
455
  requirements: []
454
- rubygems_version: 3.2.20
456
+ rubygems_version: 3.3.3
455
457
  signing_key:
456
458
  specification_version: 4
457
459
  summary: This gem is used to get quickly started with the Shopify API