shopify_app 19.0.2 → 20.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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?
@@ -6,7 +6,7 @@ module ShopifyApp
6
6
  ShopifyApp.configuration.webhooks_manager_queue_name
7
7
  end
8
8
 
9
- def perform(shop_domain:, shop_token:)
9
+ def perform(shop_domain:, shop_token:, **)
10
10
  ShopifyAPI::Auth::Session.temp(shop: shop_domain, access_token: shop_token) do |session|
11
11
  WebhooksManager.create_webhooks(session: session)
12
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyApp
4
- VERSION = "19.0.2"
4
+ VERSION = "20.0.1"
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.1",
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")
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: 20.0.1
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-07-06 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