shopify_app 22.5.2 → 23.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/dependabot.yaml +6 -0
- data/.github/workflows/build.yml +5 -5
- data/.github/workflows/close-waiting-for-response-issues.yml +1 -1
- data/.github/workflows/release.yml +2 -2
- data/.github/workflows/remove-labels-on-activity.yml +2 -3
- data/.github/workflows/rubocop.yml +2 -2
- data/CHANGELOG.md +22 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +181 -122
- data/README.md +105 -25
- data/Rakefile +13 -0
- data/app/controllers/shopify_app/callback_controller.rb +2 -40
- data/app/controllers/shopify_app/extension_verification_controller.rb +1 -1
- data/app/jobs/shopify_app/script_tags_manager_job.rb +16 -0
- data/docs/Releasing.md +113 -19
- data/docs/Upgrading.md +72 -0
- data/docs/shopify_app/content-security-policy.md +50 -3
- data/docs/shopify_app/controller-concerns.md +20 -0
- data/docs/shopify_app/script-tags.md +52 -0
- data/docs/shopify_app/sessions.md +149 -22
- data/lib/generators/shopify_app/add_app_uninstalled_job/templates/app_uninstalled_job.rb.tt +2 -2
- data/lib/generators/shopify_app/add_privacy_jobs/templates/customers_data_request_job.rb.tt +2 -2
- data/lib/generators/shopify_app/add_privacy_jobs/templates/customers_redact_job.rb.tt +2 -2
- data/lib/generators/shopify_app/add_privacy_jobs/templates/shop_redact_job.rb.tt +2 -2
- data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +2 -2
- data/lib/generators/shopify_app/install/install_generator.rb +1 -1
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +18 -0
- data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_token_expiry_columns.erb +7 -0
- data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -1
- data/lib/generators/shopify_app/user_model/templates/user.rb +1 -1
- data/lib/shopify_app/auth/post_authenticate_tasks.rb +8 -0
- data/lib/shopify_app/auth/token_exchange.rb +7 -0
- data/lib/shopify_app/configuration.rb +5 -5
- data/lib/shopify_app/controller_concerns/login_protection.rb +12 -8
- data/lib/shopify_app/engine.rb +2 -5
- data/lib/shopify_app/errors.rb +2 -0
- data/lib/shopify_app/managers/script_tags_manager.rb +348 -0
- data/lib/shopify_app/session/shop_session_storage.rb +84 -2
- data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +6 -0
- data/lib/shopify_app/session/user_session_storage.rb +21 -2
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +6 -0
- data/lib/shopify_app/utils.rb +1 -1
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app.rb +12 -7
- data/package.json +1 -1
- data/shopify_app.gemspec +9 -10
- data/translation.yml +1 -0
- data/yarn.lock +7 -9
- metadata +63 -46
- data/lib/shopify_app/middleware/jwt_middleware.rb +0 -48
- data/lib/shopify_app/session/jwt.rb +0 -73
- /data/{lib/shopify_app/jobs → app/jobs/shopify_app}/webhooks_manager_job.rb +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
class ScriptTagsManager
|
|
5
|
+
def self.queue(shop_domain, shop_token, script_tags)
|
|
6
|
+
ShopifyApp::ScriptTagsManagerJob.perform_later(
|
|
7
|
+
shop_domain: shop_domain,
|
|
8
|
+
shop_token: shop_token,
|
|
9
|
+
# Procs cannot be serialized so we interpolate now, if necessary
|
|
10
|
+
script_tags: build_src(script_tags, shop_domain),
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.build_src(script_tags, domain)
|
|
15
|
+
script_tags.map do |tag|
|
|
16
|
+
next tag unless tag[:src].respond_to?(:call)
|
|
17
|
+
|
|
18
|
+
tag = tag.dup
|
|
19
|
+
tag[:src] = tag[:src].call(domain)
|
|
20
|
+
tag
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :required_script_tags, :shop_domain
|
|
25
|
+
|
|
26
|
+
def initialize(script_tags, shop_domain)
|
|
27
|
+
@required_script_tags = script_tags
|
|
28
|
+
@shop_domain = shop_domain
|
|
29
|
+
@session = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def recreate_script_tags!(session:)
|
|
33
|
+
destroy_script_tags(session: session)
|
|
34
|
+
create_script_tags(session: session)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_script_tags(session:)
|
|
38
|
+
@session = session
|
|
39
|
+
return unless required_script_tags.present?
|
|
40
|
+
|
|
41
|
+
template_types_to_check = required_script_tags.flat_map { |tag| tag[:template_types] }.compact.uniq
|
|
42
|
+
|
|
43
|
+
if template_types_to_check.any?
|
|
44
|
+
active_theme = fetch_active_theme
|
|
45
|
+
unless active_theme
|
|
46
|
+
ShopifyApp::Logger.debug("Failed to fetch active theme. Skipping script tag creation.")
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if all_templates_support_app_blocks?(active_theme["id"], template_types_to_check)
|
|
51
|
+
ShopifyApp::Logger.info(
|
|
52
|
+
"Theme supports app blocks for templates: #{template_types_to_check.join(", ")}. " \
|
|
53
|
+
"Skipping script tag creation.",
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
expanded_script_tags.each do |script_tag|
|
|
60
|
+
create_script_tag(script_tag) unless script_tag_exists?(script_tag[:src])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def destroy_script_tags(session:)
|
|
65
|
+
@session = session
|
|
66
|
+
script_tags = expanded_script_tags
|
|
67
|
+
fetch_all_script_tags.each do |tag|
|
|
68
|
+
delete_script_tag(tag) if required_script_tag?(script_tags, tag)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@current_script_tags = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
FILES_QUERY = <<~QUERY
|
|
77
|
+
query getFiles($themeId: ID!, $filenames: [String!]!) {
|
|
78
|
+
theme(id: $themeId) {
|
|
79
|
+
files(filenames: $filenames) {
|
|
80
|
+
nodes {
|
|
81
|
+
filename
|
|
82
|
+
body {
|
|
83
|
+
... on OnlineStoreThemeFileBodyText {
|
|
84
|
+
content
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
QUERY
|
|
92
|
+
|
|
93
|
+
ACTIVE_THEME_QUERY = <<~QUERY
|
|
94
|
+
{
|
|
95
|
+
themes(first: 1, roles: [MAIN]) {
|
|
96
|
+
nodes {
|
|
97
|
+
id
|
|
98
|
+
name
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
QUERY
|
|
103
|
+
|
|
104
|
+
SCRIPT_TAG_CREATE_MUTATION = <<~QUERY
|
|
105
|
+
mutation ScriptTagCreate($input: ScriptTagInput!) {
|
|
106
|
+
scriptTagCreate(input: $input) {
|
|
107
|
+
scriptTag {
|
|
108
|
+
id
|
|
109
|
+
src
|
|
110
|
+
displayScope
|
|
111
|
+
cache
|
|
112
|
+
}
|
|
113
|
+
userErrors {
|
|
114
|
+
field
|
|
115
|
+
message
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
QUERY
|
|
120
|
+
|
|
121
|
+
SCRIPT_TAG_DELETE_MUTATION = <<~QUERY
|
|
122
|
+
mutation scriptTagDelete($id: ID!) {
|
|
123
|
+
scriptTagDelete(id: $id) {
|
|
124
|
+
deletedScriptTagId
|
|
125
|
+
userErrors {
|
|
126
|
+
field
|
|
127
|
+
message
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
QUERY
|
|
132
|
+
|
|
133
|
+
SCRIPT_TAGS_QUERY = <<~QUERY
|
|
134
|
+
{
|
|
135
|
+
scriptTags(first: 250) {
|
|
136
|
+
edges {
|
|
137
|
+
node {
|
|
138
|
+
id
|
|
139
|
+
src
|
|
140
|
+
displayScope
|
|
141
|
+
cache
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
QUERY
|
|
147
|
+
|
|
148
|
+
def fetch_active_theme
|
|
149
|
+
client = graphql_client
|
|
150
|
+
|
|
151
|
+
response = client.query(query: ACTIVE_THEME_QUERY)
|
|
152
|
+
|
|
153
|
+
if response.body["errors"].present?
|
|
154
|
+
error_message = response.body["errors"].map { |e| e["message"] }.join(", ")
|
|
155
|
+
raise ShopifyAPI::Errors::InvalidGraphqlRequestError, error_message
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
themes = response.body["data"]["themes"]["nodes"]
|
|
159
|
+
return if themes.empty?
|
|
160
|
+
|
|
161
|
+
themes.first
|
|
162
|
+
rescue => e
|
|
163
|
+
ShopifyApp::Logger.warn("Failed to fetch active theme: #{e.message}")
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def all_templates_support_app_blocks?(theme_id, template_types)
|
|
168
|
+
client = graphql_client
|
|
169
|
+
|
|
170
|
+
template_filenames = template_types.map { |type| "templates/#{type}.json" }
|
|
171
|
+
json_templates = fetch_json_templates(client, theme_id, template_filenames)
|
|
172
|
+
|
|
173
|
+
return false if json_templates.length != template_types.length
|
|
174
|
+
|
|
175
|
+
main_sections = extract_main_sections(json_templates)
|
|
176
|
+
|
|
177
|
+
return false if main_sections.length != template_types.length
|
|
178
|
+
|
|
179
|
+
all_sections_support_app_blocks?(client, theme_id, main_sections)
|
|
180
|
+
rescue => e
|
|
181
|
+
ShopifyApp::Logger.error("Error checking template support: #{e.message}")
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def fetch_json_templates(client, theme_id, template_filenames)
|
|
186
|
+
files_variables = {
|
|
187
|
+
themeId: theme_id,
|
|
188
|
+
filenames: template_filenames,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
files_response = client.query(query: FILES_QUERY, variables: files_variables)
|
|
192
|
+
|
|
193
|
+
if files_response.body["errors"].present?
|
|
194
|
+
error_message = files_response.body["errors"].map { |e| e["message"] }.join(", ")
|
|
195
|
+
raise ShopifyAPI::Errors::InvalidGraphqlRequestError, error_message
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
files_response.body["data"]["theme"]["files"]["nodes"]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def extract_main_sections(json_templates)
|
|
202
|
+
main_sections = []
|
|
203
|
+
|
|
204
|
+
json_templates.each do |template|
|
|
205
|
+
template_content = template["body"]["content"]
|
|
206
|
+
template_data = JSON.parse(template_content)
|
|
207
|
+
|
|
208
|
+
main_section = nil
|
|
209
|
+
template_data["sections"].each do |id, section|
|
|
210
|
+
if id == "main" || section["type"].to_s.start_with?("main-")
|
|
211
|
+
main_section = "sections/#{section["type"]}.liquid"
|
|
212
|
+
break
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
main_sections << main_section if main_section
|
|
217
|
+
rescue => e
|
|
218
|
+
ShopifyApp::Logger.error("Error extracting main section from template #{template["filename"]}: #{e.message}")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
main_sections
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def all_sections_support_app_blocks?(client, theme_id, section_filenames)
|
|
225
|
+
return false if section_filenames.empty?
|
|
226
|
+
|
|
227
|
+
section_variables = {
|
|
228
|
+
themeId: theme_id,
|
|
229
|
+
filenames: section_filenames,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
section_response = client.query(query: FILES_QUERY, variables: section_variables)
|
|
233
|
+
|
|
234
|
+
if section_response.body["errors"].present?
|
|
235
|
+
error_message = section_response.body["errors"].map { |e| e["message"] }.join(", ")
|
|
236
|
+
raise ShopifyAPI::Errors::InvalidGraphqlRequestError, error_message
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
section_files = section_response.body["data"]["theme"]["files"]["nodes"]
|
|
240
|
+
|
|
241
|
+
return false if section_files.length != section_filenames.length
|
|
242
|
+
|
|
243
|
+
# Check if all sections support app blocks
|
|
244
|
+
section_files.all? do |file|
|
|
245
|
+
section_content = file["body"]["content"]
|
|
246
|
+
schema_match = section_content.match(/\{\%\s+schema\s+\%\}([\s\S]*?)\{\%\s+endschema\s+\%\}/m)
|
|
247
|
+
next false unless schema_match
|
|
248
|
+
|
|
249
|
+
schema = JSON.parse(schema_match[1])
|
|
250
|
+
schema["blocks"]&.any? { |block| block["type"] == "@app" } || false
|
|
251
|
+
end
|
|
252
|
+
rescue => e
|
|
253
|
+
ShopifyApp::Logger.error("Error checking section support: #{e.message}")
|
|
254
|
+
false
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def expanded_script_tags
|
|
258
|
+
self.class.build_src(required_script_tags, shop_domain)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def required_script_tag?(script_tags, tag)
|
|
262
|
+
script_tags.map { |w| w[:src] }.include?(tag["src"])
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def create_script_tag(attributes)
|
|
266
|
+
client = graphql_client
|
|
267
|
+
|
|
268
|
+
variables = {
|
|
269
|
+
input: {
|
|
270
|
+
src: attributes[:src],
|
|
271
|
+
displayScope: "ONLINE_STORE",
|
|
272
|
+
cache: attributes[:cache] || false,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
response = client.query(query: SCRIPT_TAG_CREATE_MUTATION, variables: variables)
|
|
277
|
+
|
|
278
|
+
if response.body["errors"].present?
|
|
279
|
+
error_messages = response.body["errors"].map { |e| e["message"] }.join(", ")
|
|
280
|
+
raise ::ShopifyApp::CreationFailed, "ScriptTag creation failed: #{error_messages}"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
if response.body["data"]["scriptTagCreate"]["userErrors"].any?
|
|
284
|
+
errors = response.body["data"]["scriptTagCreate"]["userErrors"]
|
|
285
|
+
error_messages = errors.map { |e| "#{e["field"]}: #{e["message"]}" }.join(", ")
|
|
286
|
+
raise ::ShopifyApp::CreationFailed, "ScriptTag creation failed: #{error_messages}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
response.body["data"]["scriptTagCreate"]["scriptTag"]
|
|
290
|
+
rescue ShopifyAPI::Errors::HttpResponseError => e
|
|
291
|
+
raise ::ShopifyApp::CreationFailed, e.message
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def delete_script_tag(tag)
|
|
295
|
+
client = graphql_client
|
|
296
|
+
|
|
297
|
+
variables = { id: tag["id"] }
|
|
298
|
+
|
|
299
|
+
response = client.query(query: SCRIPT_TAG_DELETE_MUTATION, variables: variables)
|
|
300
|
+
|
|
301
|
+
if response.body["errors"].present?
|
|
302
|
+
error_messages = response.body["errors"].map { |e| e["message"] }.join(", ")
|
|
303
|
+
ShopifyApp::Logger.error("Failed to delete script tag: #{error_messages}")
|
|
304
|
+
return
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if response.body["data"]["scriptTagDelete"]["userErrors"].any?
|
|
308
|
+
errors = response.body["data"]["scriptTagDelete"]["userErrors"]
|
|
309
|
+
error_messages = errors.map { |e| "#{e["field"]}: #{e["message"]}" }.join(", ")
|
|
310
|
+
ShopifyApp::Logger.error("Failed to delete script tag: #{error_messages}")
|
|
311
|
+
return
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
response.body["data"]["scriptTagDelete"]["deletedScriptTagId"]
|
|
315
|
+
rescue ShopifyAPI::Errors::HttpResponseError => e
|
|
316
|
+
ShopifyApp::Logger.error("Failed to delete script tag: #{e.message}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def script_tag_exists?(src)
|
|
320
|
+
current_script_tags[src]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def current_script_tags
|
|
324
|
+
@current_script_tags ||= fetch_all_script_tags.index_by { |tag| tag["src"] }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def fetch_all_script_tags
|
|
328
|
+
client = graphql_client
|
|
329
|
+
|
|
330
|
+
response = client.query(query: SCRIPT_TAGS_QUERY)
|
|
331
|
+
|
|
332
|
+
if response.body["errors"].present?
|
|
333
|
+
error_messages = response.body["errors"].map { |e| e["message"] }.join(", ")
|
|
334
|
+
ShopifyApp::Logger.warn("GraphQL error fetching script tags: #{error_messages}")
|
|
335
|
+
return []
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
response.body["data"]["scriptTags"]["edges"].map { |edge| edge["node"] }
|
|
339
|
+
rescue => e
|
|
340
|
+
ShopifyApp::Logger.warn("Error fetching script tags: #{e.message}")
|
|
341
|
+
[]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def graphql_client
|
|
345
|
+
ShopifyAPI::Clients::Graphql::Admin.new(session: @session)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
@@ -9,10 +9,46 @@ module ShopifyApp
|
|
|
9
9
|
validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def with_shopify_session(auto_refresh: true, &block)
|
|
13
|
+
refresh_token_if_expired! if auto_refresh
|
|
14
|
+
super(&block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def refresh_token_if_expired!
|
|
18
|
+
return unless should_refresh?
|
|
19
|
+
raise RefreshTokenExpiredError if refresh_token_expired?
|
|
20
|
+
|
|
21
|
+
# Acquire row lock to prevent concurrent refreshes
|
|
22
|
+
with_lock do
|
|
23
|
+
reload
|
|
24
|
+
# Check again after lock - token might have been refreshed by another process
|
|
25
|
+
return unless should_refresh?
|
|
26
|
+
|
|
27
|
+
perform_token_refresh!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
12
31
|
class_methods do
|
|
13
32
|
def store(auth_session, *_args)
|
|
14
33
|
shop = find_or_initialize_by(shopify_domain: auth_session.shop)
|
|
15
34
|
shop.shopify_token = auth_session.access_token
|
|
35
|
+
|
|
36
|
+
if shop.has_attribute?(:access_scopes)
|
|
37
|
+
shop.access_scopes = auth_session.scope.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if shop.has_attribute?(:expires_at)
|
|
41
|
+
shop.expires_at = auth_session.expires
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if shop.has_attribute?(:refresh_token)
|
|
45
|
+
shop.refresh_token = auth_session.refresh_token
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if shop.has_attribute?(:refresh_token_expires_at)
|
|
49
|
+
shop.refresh_token_expires_at = auth_session.refresh_token_expires
|
|
50
|
+
end
|
|
51
|
+
|
|
16
52
|
shop.save!
|
|
17
53
|
shop.id
|
|
18
54
|
end
|
|
@@ -36,11 +72,57 @@ module ShopifyApp
|
|
|
36
72
|
def construct_session(shop)
|
|
37
73
|
return unless shop
|
|
38
74
|
|
|
39
|
-
|
|
75
|
+
session_attrs = {
|
|
40
76
|
shop: shop.shopify_domain,
|
|
41
77
|
access_token: shop.shopify_token,
|
|
42
|
-
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if shop.has_attribute?(:access_scopes)
|
|
81
|
+
session_attrs[:scope] = shop.access_scopes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if shop.has_attribute?(:expires_at)
|
|
85
|
+
session_attrs[:expires] = shop.expires_at
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if shop.has_attribute?(:refresh_token)
|
|
89
|
+
session_attrs[:refresh_token] = shop.refresh_token
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if shop.has_attribute?(:refresh_token_expires_at)
|
|
93
|
+
session_attrs[:refresh_token_expires] = shop.refresh_token_expires_at
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
ShopifyAPI::Auth::Session.new(**session_attrs)
|
|
43
97
|
end
|
|
44
98
|
end
|
|
99
|
+
|
|
100
|
+
def perform_token_refresh!
|
|
101
|
+
new_session = ShopifyAPI::Auth::RefreshToken.refresh_access_token(
|
|
102
|
+
shop: shopify_domain,
|
|
103
|
+
refresh_token: refresh_token,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
update!(
|
|
107
|
+
shopify_token: new_session.access_token,
|
|
108
|
+
expires_at: new_session.expires,
|
|
109
|
+
refresh_token: new_session.refresh_token,
|
|
110
|
+
refresh_token_expires_at: new_session.refresh_token_expires,
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def should_refresh?
|
|
115
|
+
return false unless has_attribute?(:expires_at) && expires_at.present?
|
|
116
|
+
return false unless has_attribute?(:refresh_token) && refresh_token.present?
|
|
117
|
+
return false unless has_attribute?(:refresh_token_expires_at) && refresh_token_expires_at.present?
|
|
118
|
+
|
|
119
|
+
expires_at <= Time.now
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def refresh_token_expired?
|
|
123
|
+
return false unless has_attribute?(:refresh_token_expires_at) && refresh_token_expires_at.present?
|
|
124
|
+
|
|
125
|
+
refresh_token_expires_at <= Time.now
|
|
126
|
+
end
|
|
45
127
|
end
|
|
46
128
|
end
|
|
@@ -6,6 +6,12 @@ module ShopifyApp
|
|
|
6
6
|
include ::ShopifyApp::SessionStorage
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
+
ShopifyApp::Logger.deprecated(
|
|
10
|
+
"ShopSessionStorageWithScopes is deprecated and will be removed in v24.0.0. " \
|
|
11
|
+
"Use ShopSessionStorage instead, which now handles access_scopes, expires_at, " \
|
|
12
|
+
"refresh_token, and refresh_token_expires_at automatically.",
|
|
13
|
+
"24.0.0",
|
|
14
|
+
)
|
|
9
15
|
validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false }
|
|
10
16
|
end
|
|
11
17
|
|
|
@@ -14,6 +14,15 @@ module ShopifyApp
|
|
|
14
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
|
+
|
|
18
|
+
if user.has_attribute?(:access_scopes)
|
|
19
|
+
user.access_scopes = auth_session.scope.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if user.has_attribute?(:expires_at)
|
|
23
|
+
user.expires_at = auth_session.expires
|
|
24
|
+
end
|
|
25
|
+
|
|
17
26
|
user.save!
|
|
18
27
|
user.id
|
|
19
28
|
end
|
|
@@ -48,11 +57,21 @@ module ShopifyApp
|
|
|
48
57
|
collaborator: false,
|
|
49
58
|
)
|
|
50
59
|
|
|
51
|
-
|
|
60
|
+
session_attrs = {
|
|
52
61
|
shop: user.shopify_domain,
|
|
53
62
|
access_token: user.shopify_token,
|
|
54
63
|
associated_user: associated_user,
|
|
55
|
-
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if user.has_attribute?(:access_scopes)
|
|
67
|
+
session_attrs[:scope] = user.access_scopes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if user.has_attribute?(:expires_at)
|
|
71
|
+
session_attrs[:expires] = user.expires_at
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
ShopifyAPI::Auth::Session.new(**session_attrs)
|
|
56
75
|
end
|
|
57
76
|
end
|
|
58
77
|
end
|
|
@@ -6,6 +6,12 @@ module ShopifyApp
|
|
|
6
6
|
include ::ShopifyApp::SessionStorage
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
+
ShopifyApp::Logger.deprecated(
|
|
10
|
+
"UserSessionStorageWithScopes is deprecated and will be removed in v24.0.0. " \
|
|
11
|
+
"Use UserSessionStorage instead, which now handles access_scopes and expires_at automatically.",
|
|
12
|
+
"24.0.0",
|
|
13
|
+
)
|
|
14
|
+
|
|
9
15
|
validates :shopify_domain, presence: true
|
|
10
16
|
end
|
|
11
17
|
|
data/lib/shopify_app/utils.rb
CHANGED
|
@@ -13,7 +13,7 @@ module ShopifyApp
|
|
|
13
13
|
|
|
14
14
|
def sanitize_shop_domain(shop_domain)
|
|
15
15
|
uri = uri_from_shop_domain(shop_domain)
|
|
16
|
-
return
|
|
16
|
+
return if uri.nil? || uri.host.nil?
|
|
17
17
|
|
|
18
18
|
trusted_domains.each do |trusted_domain|
|
|
19
19
|
no_shop_name_in_subdomain = uri.host == trusted_domain
|
data/lib/shopify_app/version.rb
CHANGED
data/lib/shopify_app.rb
CHANGED
|
@@ -26,6 +26,17 @@ module ShopifyApp
|
|
|
26
26
|
!configuration.disable_webpacker
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def self.add_csp_directives(policy)
|
|
30
|
+
# Get current script-src directives
|
|
31
|
+
current_script_src = policy.directives["script-src"] || []
|
|
32
|
+
|
|
33
|
+
# Add App Bridge script source if not already present
|
|
34
|
+
app_bridge_url = "https://cdn.shopify.com/shopifycloud/app-bridge.js"
|
|
35
|
+
unless current_script_src.include?(app_bridge_url)
|
|
36
|
+
policy.script_src(*current_script_src, app_bridge_url)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
29
40
|
# config
|
|
30
41
|
require "shopify_app/configuration"
|
|
31
42
|
|
|
@@ -62,20 +73,14 @@ module ShopifyApp
|
|
|
62
73
|
require "shopify_app/auth/post_authenticate_tasks"
|
|
63
74
|
require "shopify_app/auth/token_exchange"
|
|
64
75
|
|
|
65
|
-
# jobs
|
|
66
|
-
require "shopify_app/jobs/webhooks_manager_job"
|
|
67
|
-
|
|
68
76
|
# managers
|
|
69
77
|
require "shopify_app/managers/webhooks_manager"
|
|
70
|
-
|
|
71
|
-
# middleware
|
|
72
|
-
require "shopify_app/middleware/jwt_middleware"
|
|
78
|
+
require "shopify_app/managers/script_tags_manager"
|
|
73
79
|
|
|
74
80
|
# session
|
|
75
81
|
require "shopify_app/session/in_memory_session_store"
|
|
76
82
|
require "shopify_app/session/in_memory_shop_session_store"
|
|
77
83
|
require "shopify_app/session/in_memory_user_session_store"
|
|
78
|
-
require "shopify_app/session/jwt"
|
|
79
84
|
require "shopify_app/session/null_user_session_store"
|
|
80
85
|
require "shopify_app/session/session_repository"
|
|
81
86
|
require "shopify_app/session/session_storage"
|
data/package.json
CHANGED
data/shopify_app.gemspec
CHANGED
|
@@ -10,29 +10,28 @@ 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 = ">= 3.
|
|
13
|
+
s.required_ruby_version = ">= 3.2"
|
|
14
14
|
|
|
15
15
|
s.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
16
16
|
|
|
17
|
-
s.add_runtime_dependency("activeresource") # TODO: Remove this once all active resource dependencies are removed
|
|
18
17
|
s.add_runtime_dependency("addressable", "~> 2.7")
|
|
19
|
-
s.add_runtime_dependency("rails", "
|
|
18
|
+
s.add_runtime_dependency("rails", ">= 7.1", "< 9")
|
|
20
19
|
s.add_runtime_dependency("redirect_safely", "~> 1.0")
|
|
21
|
-
s.add_runtime_dependency("shopify_api", "
|
|
22
|
-
s.add_runtime_dependency("sprockets-rails"
|
|
23
|
-
# Deprecated: move to development dependencies when releasing v23
|
|
24
|
-
s.add_runtime_dependency("jwt", ">= 2.2.3")
|
|
25
|
-
|
|
20
|
+
s.add_runtime_dependency("shopify_api", "~> 16.0")
|
|
21
|
+
s.add_runtime_dependency("sprockets-rails")
|
|
26
22
|
s.add_development_dependency("byebug")
|
|
23
|
+
s.add_development_dependency("csv")
|
|
24
|
+
s.add_development_dependency("jwt", ">= 2.2.3")
|
|
27
25
|
s.add_development_dependency("minitest")
|
|
28
|
-
s.add_development_dependency("mocha")
|
|
26
|
+
s.add_development_dependency("mocha", ">= 2.1.0")
|
|
27
|
+
s.add_development_dependency("mutex_m")
|
|
29
28
|
s.add_development_dependency("pry")
|
|
30
29
|
s.add_development_dependency("pry-nav")
|
|
31
30
|
s.add_development_dependency("pry-stack_explorer")
|
|
32
31
|
s.add_development_dependency("rake")
|
|
33
32
|
s.add_development_dependency("rb-readline")
|
|
34
33
|
s.add_development_dependency("ruby-lsp")
|
|
35
|
-
s.add_development_dependency("sqlite3"
|
|
34
|
+
s.add_development_dependency("sqlite3")
|
|
36
35
|
s.add_development_dependency("webmock")
|
|
37
36
|
|
|
38
37
|
s.files = %x(git ls-files).split("\n").reject { |f| f.match(%r{^(test|example)/}) }
|
data/translation.yml
CHANGED
data/yarn.lock
CHANGED
|
@@ -2644,9 +2644,9 @@ mime@^2.5.2:
|
|
|
2644
2644
|
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
|
|
2645
2645
|
|
|
2646
2646
|
min-document@^2.19.0:
|
|
2647
|
-
version "2.19.
|
|
2648
|
-
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.
|
|
2649
|
-
integrity sha512-
|
|
2647
|
+
version "2.19.2"
|
|
2648
|
+
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.2.tgz#f95db44639eaae3ac8ea85ae6809ae85ff7e3b81"
|
|
2649
|
+
integrity sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==
|
|
2650
2650
|
dependencies:
|
|
2651
2651
|
dom-walk "^0.1.0"
|
|
2652
2652
|
|
|
@@ -3022,7 +3022,7 @@ rfdc@^1.3.0:
|
|
|
3022
3022
|
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
|
3023
3023
|
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
|
3024
3024
|
|
|
3025
|
-
rimraf@^3.0.
|
|
3025
|
+
rimraf@^3.0.2:
|
|
3026
3026
|
version "3.0.2"
|
|
3027
3027
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
|
3028
3028
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
|
@@ -3280,11 +3280,9 @@ terser@^5.31.1:
|
|
|
3280
3280
|
source-map-support "~0.5.20"
|
|
3281
3281
|
|
|
3282
3282
|
tmp@^0.2.1:
|
|
3283
|
-
version "0.2.
|
|
3284
|
-
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.
|
|
3285
|
-
integrity sha512-
|
|
3286
|
-
dependencies:
|
|
3287
|
-
rimraf "^3.0.0"
|
|
3283
|
+
version "0.2.4"
|
|
3284
|
+
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.4.tgz#c6db987a2ccc97f812f17137b36af2b6521b0d13"
|
|
3285
|
+
integrity sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==
|
|
3288
3286
|
|
|
3289
3287
|
to-fast-properties@^2.0.0:
|
|
3290
3288
|
version "2.0.0"
|