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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +1 -1
- data/CHANGELOG.md +25 -3
- data/Gemfile.lock +11 -11
- data/README.md +0 -1
- data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +10 -0
- data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +1 -1
- data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +1 -0
- data/app/assets/javascripts/shopify_app/redirect.js +6 -8
- data/app/controllers/concerns/shopify_app/authenticated.rb +3 -0
- data/app/controllers/shopify_app/callback_controller.rb +28 -1
- data/app/controllers/shopify_app/sessions_controller.rb +3 -1
- data/config/routes.rb +17 -3
- data/docs/Releasing.md +1 -1
- data/docs/Upgrading.md +30 -17
- 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 +26 -0
- data/lib/shopify_app/session/session_repository.rb +1 -1
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +1 -1
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app.rb +1 -0
- data/package.json +1 -1
- data/shopify_app.gemspec +3 -3
- data/yarn.lock +3 -3
- metadata +11 -9
- data/app/assets/javascripts/shopify_app/app_bridge_2.0.12.js +0 -10
@@ -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
|
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
|
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
|
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
@@ -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.
|
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", "~>
|
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", "~>
|
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.
|
3519
|
-
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.
|
3520
|
-
integrity sha512-
|
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:
|
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
|
+
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:
|
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
|
@@ -86,14 +86,14 @@ dependencies:
|
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
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: '
|
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/
|
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.
|
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.
|
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
|