shopify_app 21.0.0 → 22.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/ENHANCEMENT.md +9 -0
  4. data/.github/ISSUE_TEMPLATE/bug-report.md +30 -47
  5. data/.github/ISSUE_TEMPLATE/feature-request.md +5 -29
  6. data/.github/workflows/build.yml +11 -12
  7. data/.github/workflows/release.yml +2 -2
  8. data/.github/workflows/remove-labels-on-activity.yml +1 -1
  9. data/.github/workflows/rubocop.yml +2 -3
  10. data/.nvmrc +1 -1
  11. data/.rubocop.yml +2 -1
  12. data/.ruby-version +1 -1
  13. data/.spin/rails/prepare-application +8 -0
  14. data/CHANGELOG.md +173 -7
  15. data/CODE_OF_CONDUCT.md +46 -0
  16. data/CONTRIBUTING.md +16 -6
  17. data/Gemfile +1 -0
  18. data/Gemfile.lock +160 -121
  19. data/README.md +67 -19
  20. data/SECURITY.md +1 -1
  21. data/app/assets/javascripts/shopify_app/redirect.js +3 -10
  22. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +9 -4
  23. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +25 -0
  24. data/app/controllers/concerns/shopify_app/ensure_installed.rb +84 -0
  25. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +5 -1
  26. data/app/controllers/shopify_app/authenticated_controller.rb +1 -1
  27. data/app/controllers/shopify_app/callback_controller.rb +101 -39
  28. data/app/controllers/shopify_app/extension_verification_controller.rb +4 -1
  29. data/app/controllers/shopify_app/sessions_controller.rb +37 -7
  30. data/app/controllers/shopify_app/webhooks_controller.rb +1 -1
  31. data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
  32. data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
  33. data/app/views/shopify_app/shared/redirect.html.erb +10 -1
  34. data/config/locales/cs.yml +0 -18
  35. data/config/locales/da.yml +0 -15
  36. data/config/locales/de.yml +0 -17
  37. data/config/locales/en.yml +0 -11
  38. data/config/locales/es.yml +0 -17
  39. data/config/locales/fi.yml +0 -15
  40. data/config/locales/fr.yml +0 -18
  41. data/config/locales/it.yml +0 -16
  42. data/config/locales/ja.yml +0 -12
  43. data/config/locales/ko.yml +0 -14
  44. data/config/locales/nb.yml +0 -16
  45. data/config/locales/nl.yml +0 -16
  46. data/config/locales/pl.yml +0 -16
  47. data/config/locales/pt-BR.yml +0 -16
  48. data/config/locales/pt-PT.yml +0 -17
  49. data/config/locales/sv.yml +0 -16
  50. data/config/locales/th.yml +0 -15
  51. data/config/locales/tr.yml +0 -17
  52. data/config/locales/vi.yml +0 -17
  53. data/config/locales/zh-CN.yml +0 -11
  54. data/config/locales/zh-TW.yml +0 -11
  55. data/config/routes.rb +2 -1
  56. data/docs/Quickstart.md +14 -5
  57. data/docs/Troubleshooting.md +38 -25
  58. data/docs/Upgrading.md +103 -32
  59. data/docs/shopify_app/authentication.md +179 -58
  60. data/docs/shopify_app/controller-concerns.md +89 -0
  61. data/docs/shopify_app/engine.md +2 -11
  62. data/docs/shopify_app/generators.md +2 -2
  63. data/docs/shopify_app/logging.md +21 -0
  64. data/docs/shopify_app/sessions.md +358 -0
  65. data/docs/shopify_app/testing.md +32 -10
  66. data/docs/shopify_app/webhooks.md +97 -7
  67. data/karma.conf.js +6 -4
  68. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +6 -3
  69. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -1
  70. data/lib/generators/shopify_app/add_app_uninstalled_job/add_app_uninstalled_job_generator.rb +15 -0
  71. data/lib/generators/shopify_app/add_app_uninstalled_job/templates/app_uninstalled_job.rb.tt +22 -0
  72. data/lib/generators/shopify_app/add_declarative_webhook/add_declarative_webhook_generator.rb +53 -0
  73. data/lib/generators/shopify_app/add_declarative_webhook/templates/webhook_controller.rb.tt +13 -0
  74. data/lib/generators/shopify_app/add_declarative_webhook/templates/webhook_job.rb.tt +15 -0
  75. data/lib/generators/shopify_app/add_privacy_jobs/add_privacy_jobs_generator.rb +23 -0
  76. data/lib/generators/shopify_app/add_privacy_jobs/templates/customers_data_request_job.rb.tt +22 -0
  77. data/lib/generators/shopify_app/add_privacy_jobs/templates/customers_redact_job.rb.tt +22 -0
  78. data/lib/generators/shopify_app/add_privacy_jobs/templates/shop_redact_job.rb.tt +22 -0
  79. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +8 -3
  80. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +4 -2
  81. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +1 -1
  82. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +1 -1
  83. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
  84. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -1
  85. data/lib/generators/shopify_app/install/install_generator.rb +4 -4
  86. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +13 -3
  87. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -1
  88. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  89. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -1
  90. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +1 -1
  91. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +1 -1
  92. data/lib/generators/shopify_app/shopify_app_generator.rb +2 -0
  93. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +1 -1
  94. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_expires_at_column.erb +5 -0
  95. data/lib/generators/shopify_app/user_model/user_model_generator.rb +21 -1
  96. data/lib/shopify_app/access_scopes/noop_strategy.rb +4 -0
  97. data/lib/shopify_app/access_scopes/user_strategy.rb +9 -2
  98. data/lib/shopify_app/admin_api/with_token_refetch.rb +27 -0
  99. data/lib/shopify_app/auth/post_authenticate_tasks.rb +48 -0
  100. data/lib/shopify_app/auth/token_exchange.rb +73 -0
  101. data/lib/shopify_app/configuration.rb +82 -1
  102. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -3
  103. data/lib/shopify_app/controller_concerns/csrf_protection.rb +2 -1
  104. data/lib/shopify_app/controller_concerns/embedded_app.rb +42 -3
  105. data/lib/shopify_app/controller_concerns/ensure_billing.rb +28 -12
  106. data/lib/shopify_app/controller_concerns/frame_ancestors.rb +1 -1
  107. data/lib/shopify_app/controller_concerns/localization.rb +11 -8
  108. data/lib/shopify_app/controller_concerns/login_protection.rb +83 -38
  109. data/lib/shopify_app/controller_concerns/payload_verification.rb +1 -1
  110. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +15 -3
  111. data/lib/shopify_app/controller_concerns/sanitized_params.rb +5 -0
  112. data/lib/shopify_app/controller_concerns/token_exchange.rb +111 -0
  113. data/lib/shopify_app/controller_concerns/webhook_verification.rb +4 -1
  114. data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +48 -0
  115. data/lib/shopify_app/engine.rb +7 -8
  116. data/lib/shopify_app/logger.rb +28 -0
  117. data/lib/shopify_app/managers/webhooks_manager.rb +20 -10
  118. data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
  119. data/lib/shopify_app/session/in_memory_user_session_store.rb +1 -1
  120. data/lib/shopify_app/session/jwt.rb +11 -2
  121. data/lib/shopify_app/session/session_repository.rb +66 -14
  122. data/lib/shopify_app/session/session_storage.rb +2 -2
  123. data/lib/shopify_app/session/shop_session_storage.rb +5 -1
  124. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +5 -1
  125. data/lib/shopify_app/session/user_session_storage.rb +6 -2
  126. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +27 -2
  127. data/lib/shopify_app/test_helpers/all.rb +1 -0
  128. data/lib/shopify_app/test_helpers/shopify_session_helper.rb +16 -0
  129. data/lib/shopify_app/utils.rb +82 -20
  130. data/lib/shopify_app/version.rb +1 -1
  131. data/lib/shopify_app.rb +12 -3
  132. data/package.json +5 -6
  133. data/service.yml +0 -2
  134. data/shopify_app.gemspec +6 -5
  135. data/translation.yml +1 -0
  136. data/yarn.lock +2139 -3910
  137. metadata +78 -58
  138. data/.github/workflows/stale.yml +0 -31
  139. data/app/assets/images/storage_access.svg +0 -1
  140. data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +0 -10
  141. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +0 -22
  142. data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +0 -1
  143. data/app/assets/javascripts/shopify_app/enable_cookies.js +0 -3
  144. data/app/assets/javascripts/shopify_app/itp_helper.js +0 -40
  145. data/app/assets/javascripts/shopify_app/partition_cookies.js +0 -8
  146. data/app/assets/javascripts/shopify_app/post_redirect.js +0 -9
  147. data/app/assets/javascripts/shopify_app/request_storage_access.js +0 -3
  148. data/app/assets/javascripts/shopify_app/storage_access.js +0 -148
  149. data/app/assets/javascripts/shopify_app/storage_access_redirect.js +0 -17
  150. data/app/assets/javascripts/shopify_app/top_level.js +0 -2
  151. data/app/assets/javascripts/shopify_app/top_level_interaction.js +0 -11
  152. data/app/controllers/concerns/shopify_app/authenticated.rb +0 -19
  153. data/app/controllers/concerns/shopify_app/require_known_shop.rb +0 -48
  154. data/app/views/shopify_app/sessions/enable_cookies.html.erb +0 -70
  155. data/app/views/shopify_app/sessions/request_storage_access.html.erb +0 -68
  156. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +0 -63
  157. data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +0 -13
  158. data/docs/shopify_app/script-tags.md +0 -28
  159. data/docs/shopify_app/session-repository.md +0 -88
  160. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +0 -41
  161. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +0 -62
  162. data/lib/shopify_app/controller_concerns/itp.rb +0 -45
  163. data/lib/shopify_app/jobs/scripttags_manager_job.rb +0 -16
  164. data/lib/shopify_app/managers/scripttags_manager.rb +0 -84
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class AddDeclarativeWebhookGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+ class_option :topic, type: :string, aliases: "-t", required: true
10
+ class_option :path, type: :string, aliases: "-p", required: true
11
+
12
+ hook_for :test_framework, as: :job, in: :rails do |instance, generator|
13
+ instance.invoke(generator, [instance.send(:file_name)])
14
+ end
15
+
16
+ def add_webhook_job
17
+ namespace = ShopifyApp.configuration.webhook_jobs_namespace
18
+ @job_file_name = if namespace.present?
19
+ "#{namespace}/#{file_name}_job"
20
+ else
21
+ "#{file_name}_job"
22
+ end
23
+ @job_class_name = @job_file_name.classify
24
+ template("webhook_job.rb", "app/jobs/#{@job_file_name}.rb")
25
+ end
26
+
27
+ def add_webhook_controller
28
+ @controller_file_name = "#{file_name}_controller"
29
+ @controller_class_name = @controller_file_name.classify
30
+ template("webhook_controller.rb", "app/controllers/webhooks/#{@controller_file_name}.rb")
31
+ end
32
+
33
+ def add_webhook_route
34
+ route = "\t\t\tpost '#{file_name}', to: '#{file_name}#receive'\n"
35
+ inject_into_file("config/routes.rb", route, after: /namespace :webhooks do\n/)
36
+ end
37
+
38
+ private
39
+
40
+ def file_name
41
+ path.split("/").last
42
+ end
43
+
44
+ def topic
45
+ options["topic"]
46
+ end
47
+
48
+ def path
49
+ options["path"]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webhooks
4
+ class <%= @controller_class_name %> < ApplicationController
5
+ include ShopifyApp::WebhookVerification
6
+
7
+ def receive
8
+ webhook_request = ShopifyAPI::Webhooks::Request.new(raw_body: request.raw_post, headers: request.headers.to_h)
9
+ <%= @job_class_name %>.perform_later(shop_domain: webhook_request.shop, webhook: webhook_request.parsed_body)
10
+ head(:no_content)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class <%= @job_class_name %> < ActiveJob::Base
2
+
3
+ def perform(shop_domain:, webhook:)
4
+ shop = Shop.find_by(shopify_domain: shop_domain)
5
+
6
+ if shop.nil?
7
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
8
+
9
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
10
+ end
11
+
12
+ shop.with_shopify_session do |session|
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class AddPrivacyJobsGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def add_customer_data_request_job
11
+ template("customers_data_request_job.rb", "app/jobs/customers_data_request_job.rb")
12
+ end
13
+
14
+ def add_shop_redact_job
15
+ template("shop_redact_job.rb", "app/jobs/shop_redact_job.rb")
16
+ end
17
+
18
+ def add_customer_redact_job
19
+ template("customers_redact_job.rb", "app/jobs/customers_redact_job.rb")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ class CustomersDataRequestJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ shop.with_shopify_session do
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ class CustomersRedactJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ shop.with_shopify_session do
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ class ShopRedactJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ shop.with_shopify_session do
20
+ end
21
+ end
22
+ end
@@ -20,7 +20,7 @@ module ShopifyApp
20
20
  inject_into_file(
21
21
  "config/initializers/shopify_app.rb",
22
22
  " config.webhooks = [\n ]\n",
23
- after: /ShopifyApp\.configure.*\n/
23
+ after: /ShopifyApp\.configure.*\n/,
24
24
  )
25
25
  end
26
26
 
@@ -28,7 +28,7 @@ module ShopifyApp
28
28
  inject_into_file(
29
29
  "config/initializers/shopify_app.rb",
30
30
  webhook_config,
31
- after: "config.webhooks = ["
31
+ after: "config.webhooks = [",
32
32
  )
33
33
 
34
34
  initializer = load_initializer
@@ -39,7 +39,12 @@ module ShopifyApp
39
39
  end
40
40
 
41
41
  def add_webhook_job
42
- @job_file_name = job_file_name + "_job"
42
+ namespace = ShopifyApp.configuration.webhook_jobs_namespace
43
+ @job_file_name = if namespace.present?
44
+ "#{namespace}/#{job_file_name}_job"
45
+ else
46
+ "#{job_file_name}_job"
47
+ end
43
48
  @job_class_name = @job_file_name.classify
44
49
  template("webhook_job.rb", "app/jobs/#{@job_file_name}.rb")
45
50
  end
@@ -12,10 +12,12 @@ class <%= @job_class_name %> < ActiveJob::Base
12
12
 
13
13
  if shop.nil?
14
14
  logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
- return
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
16
17
  end
17
18
 
18
- shop.with_shopify_session do
19
+ shop.with_shopify_session do |session|
20
+ ## webhook processing logic
19
21
  end
20
22
  end
21
23
  end
@@ -19,7 +19,7 @@ module ShopifyApp
19
19
  inject_into_file(
20
20
  "config/routes.rb",
21
21
  File.read(File.expand_path(find_in_source_paths("app_proxy_route.rb"))),
22
- after: "mount ShopifyApp::Engine, at: '/'\n"
22
+ after: "mount ShopifyApp::Engine, at: '/'\n",
23
23
  )
24
24
  end
25
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AuthenticatedController < ApplicationController
4
- include ShopifyApp::Authenticated
4
+ include ShopifyApp::EnsureHasSession
5
5
  end
@@ -24,7 +24,7 @@
24
24
  });
25
25
 
26
26
  var fetchProducts = function() {
27
- var headers = new Headers({ "Authorization": "Bearer " + window.sessionToken });
27
+ var headers = new Headers({ "Content-Type": "text/javascript", "Authorization": "Bearer " + window.sessionToken });
28
28
  return fetch("/products", { headers })
29
29
  .then(response => response.json())
30
30
  .then(data => {
@@ -2,7 +2,7 @@
2
2
 
3
3
  class HomeController < ApplicationController
4
4
  include ShopifyApp::EmbeddedApp
5
- include ShopifyApp::RequireKnownShop
5
+ include ShopifyApp::EnsureInstalled
6
6
  include ShopifyApp::ShopAccessScopesVerification
7
7
 
8
8
  def index
@@ -13,8 +13,8 @@ module ShopifyApp
13
13
  class_option :embedded, type: :string, default: "true"
14
14
  class_option :api_version, type: :string, default: nil
15
15
 
16
- NGROK_HOST = "/\[-\\w]+\\.ngrok\\.io/"
17
- CLOUDFLARE_HOST = "/\[-\\w]+\\.trycloudflare\\.com/"
16
+ NGROK_HOST = "/[-\\w]+\\.ngrok\\.io/"
17
+ CLOUDFLARE_HOST = "/[-\\w]+\\.trycloudflare\\.com/"
18
18
 
19
19
  def create_shopify_app_initializer
20
20
  @application_name = format_array_argument(options["application_name"])
@@ -66,7 +66,7 @@ module ShopifyApp
66
66
  inject_into_file(
67
67
  "config/environments/development.rb",
68
68
  comment,
69
- after: insert_after_line
69
+ after: insert_after_line,
70
70
  )
71
71
  comment
72
72
  end
@@ -78,7 +78,7 @@ module ShopifyApp
78
78
  inject_into_file(
79
79
  "config/environments/development.rb",
80
80
  host_line,
81
- after: explaination_comment
81
+ after: explaination_comment,
82
82
  )
83
83
  host_line
84
84
  end
@@ -4,11 +4,19 @@ ShopifyApp.configure do |config|
4
4
  config.scope = "<%= @scope %>" # Consult this page for more scope options:
5
5
  # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
6
6
  config.embedded_app = <%= embedded_app? %>
7
+ config.new_embedded_auth_strategy = <%= embedded_app? %>
8
+
7
9
  config.after_authenticate_job = false
8
10
  config.api_version = "<%= @api_version %>"
9
11
  config.shop_session_repository = 'Shop'
10
-
12
+ config.log_level = :info
11
13
  config.reauth_on_access_scope_changes = true
14
+ config.webhooks = [
15
+ { topic: "app/uninstalled", address: "webhooks/app_uninstalled"},
16
+ { topic: "customers/data_request", address: "webhooks/customers_data_request" },
17
+ { topic: "customers/redact", address: "webhooks/customers_redact"},
18
+ { topic: "shop/redact", address: "webhooks/shop_redact"}
19
+ ]
12
20
 
13
21
  config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence
14
22
  config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence
@@ -24,6 +32,8 @@ ShopifyApp.configure do |config|
24
32
  # amount: 5,
25
33
  # interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS,
26
34
  # currency_code: "USD", # Only supports USD for now
35
+ # trial_days: 0,
36
+ # test: !ENV['SHOPIFY_TEST_CHARGES'].nil? ? ["true", "1"].include?(ENV['SHOPIFY_TEST_CHARGES']) : !Rails.env.production?
27
37
  # )
28
38
 
29
39
  if defined? Rails::Server
@@ -38,11 +48,11 @@ Rails.application.config.after_initialize do
38
48
  api_key: ShopifyApp.configuration.api_key,
39
49
  api_secret_key: ShopifyApp.configuration.secret,
40
50
  api_version: ShopifyApp.configuration.api_version,
41
- host_name: URI(ENV.fetch('HOST', '')).host || '',
51
+ host: ENV['HOST'],
42
52
  scope: ShopifyApp.configuration.scope,
43
53
  is_private: !ENV.fetch('SHOPIFY_APP_PRIVATE_SHOP', '').empty?,
44
54
  is_embedded: ShopifyApp.configuration.embedded_app,
45
- session_storage: ShopifyApp::SessionRepository,
55
+ log_level: :info,
46
56
  logger: Rails.logger,
47
57
  private_shop: ENV.fetch('SHOPIFY_APP_PRIVATE_SHOP', nil),
48
58
  user_agent_prefix: "ShopifyApp/#{ShopifyApp::VERSION}"
@@ -6,7 +6,7 @@ namespace :shopify do
6
6
  all_active_shops.find_each do |shop|
7
7
  Shopify::RotateShopifyTokenJob.perform_later(
8
8
  shop_domain: shop.shopify_domain,
9
- refresh_token: args[:refresh_token]
9
+ refresh_token: args[:refresh_token],
10
10
  )
11
11
  end
12
12
  end
@@ -27,7 +27,7 @@ module Shopify
27
27
  private
28
28
 
29
29
  def log_error(message)
30
- Rails.logger.error(message)
30
+ ShopifyApp::Logger.error(message)
31
31
  end
32
32
 
33
33
  def no_access_token_error_message
@@ -15,7 +15,7 @@ module ShopifyApp
15
15
  gsub_file(
16
16
  "config/routes.rb",
17
17
  "mount ShopifyApp::Engine, at: '/'",
18
- ""
18
+ "",
19
19
  )
20
20
  end
21
21
 
@@ -35,7 +35,7 @@ module ShopifyApp
35
35
  if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
36
36
  migration_template(
37
37
  "db/migrate/add_shop_access_scopes_column.erb",
38
- "db/migrate/add_shop_access_scopes_column.rb"
38
+ "db/migrate/add_shop_access_scopes_column.rb",
39
39
  )
40
40
  end
41
41
  end
@@ -1,5 +1,5 @@
1
1
  class AddShopAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
2
  def change
3
- add_column :shops, :access_scopes, :string
3
+ add_column :shops, :access_scopes, :string, default: "", null: false
4
4
  end
5
5
  end
@@ -9,6 +9,8 @@ module ShopifyApp
9
9
  end
10
10
 
11
11
  def run_all_generators
12
+ generate("shopify_app:add_app_uninstalled_job")
13
+ generate("shopify_app:add_privacy_jobs")
12
14
  generate("shopify_app:install #{@opts.join(" ")}")
13
15
  generate("shopify_app:shop_model #{@opts.join(" ")}")
14
16
  generate("shopify_app:authenticated_controller")
@@ -1,5 +1,5 @@
1
1
  class AddUserAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
2
  def change
3
- add_column :users, :access_scopes, :string
3
+ add_column :users, :access_scopes, :string, default: "", null: false
4
4
  end
5
5
  end
@@ -0,0 +1,5 @@
1
+ class AddUserExpiresAtColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def change
3
+ add_column :users, :expires_at, :datetime
4
+ end
5
+ end
@@ -35,7 +35,27 @@ module ShopifyApp
35
35
  if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
36
36
  migration_template(
37
37
  "db/migrate/add_user_access_scopes_column.erb",
38
- "db/migrate/add_user_access_scopes_column.rb"
38
+ "db/migrate/add_user_access_scopes_column.rb",
39
+ )
40
+ end
41
+ end
42
+
43
+ def create_expires_at_storage_in_user_model
44
+ expires_at_column_prompt = <<~PROMPT
45
+ It is highly recommended that apps record the User session expiry date. \
46
+ This will allow to check if the session has expired and re-authenticate \
47
+ without a first call to Shopify.
48
+
49
+ After running the migration, the `check_session_expiry_date` configuration can be enabled.
50
+
51
+ The following migration will add an `expires_at` column to the User model. \
52
+ Do you want to include this migration? [y/n]
53
+ PROMPT
54
+
55
+ if new_shopify_cli_app? || Rails.env.test? || yes?(expires_at_column_prompt)
56
+ migration_template(
57
+ "db/migrate/add_user_expires_at_column.erb",
58
+ "db/migrate/add_user_expires_at_column.rb",
39
59
  )
40
60
  end
41
61
  end
@@ -7,6 +7,10 @@ module ShopifyApp
7
7
  def update_access_scopes?(*_args)
8
8
  false
9
9
  end
10
+
11
+ def covers_scopes?(*_args)
12
+ true
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -8,8 +8,15 @@ module ShopifyApp
8
8
  return update_access_scopes_for_user_id?(user_id) if user_id
9
9
  return update_access_scopes_for_shopify_user_id?(shopify_user_id) if shopify_user_id
10
10
 
11
- raise(::ShopifyApp::InvalidInput,
12
- "#update_access_scopes? requires user_id or shopify_user_id parameter inputs")
11
+ raise(
12
+ ::ShopifyApp::InvalidInput,
13
+ "#update_access_scopes? requires user_id or shopify_user_id parameter inputs",
14
+ )
15
+ end
16
+
17
+ def covers_scopes?(current_shopify_session)
18
+ # NOTE: this not Ruby's `covers?` method, it is defined in ShopifyAPI::Auth::AuthScopes
19
+ current_shopify_session.scope.to_a.empty? || current_shopify_session.scope.covers?(ShopifyAPI::Context.scope)
13
20
  end
14
21
 
15
22
  private
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module AdminAPI
5
+ module WithTokenRefetch
6
+ def with_token_refetch(session, shopify_id_token)
7
+ retrying = false if retrying.nil?
8
+ yield
9
+ rescue ShopifyAPI::Errors::HttpResponseError => error
10
+ if error.code != 401
11
+ ShopifyApp::Logger.debug("Encountered error: #{error.code} - #{error.response.inspect}, re-raising")
12
+ elsif retrying
13
+ ShopifyApp::Logger.debug("Shopify API returned a 401 Unauthorized error that was not corrected " \
14
+ "with token exchange, re-raising error")
15
+ else
16
+ retrying = true
17
+ ShopifyApp::Logger.debug("Shopify API returned a 401 Unauthorized error, exchanging token and " \
18
+ "retrying with new session")
19
+ new_session = ShopifyApp::Auth::TokenExchange.perform(shopify_id_token)
20
+ session.copy_attributes_from(new_session)
21
+ retry
22
+ end
23
+ raise
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module Auth
5
+ class PostAuthenticateTasks
6
+ class << self
7
+ def perform(session)
8
+ ShopifyApp::Logger.debug("Performing post authenticate tasks")
9
+ # Ensure we use the shop session to install webhooks
10
+ session_for_shop = session.online? ? shop_session(session) : session
11
+
12
+ install_webhooks(session_for_shop)
13
+
14
+ perform_after_authenticate_job(session)
15
+ end
16
+
17
+ private
18
+
19
+ def shop_session(session)
20
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(session.shop)
21
+ end
22
+
23
+ def install_webhooks(session)
24
+ ShopifyApp::Logger.debug("PostAuthenticateTasks: Installing webhooks")
25
+ return unless ShopifyApp.configuration.has_webhooks?
26
+
27
+ WebhooksManager.queue(session.shop, session.access_token)
28
+ end
29
+
30
+ def perform_after_authenticate_job(session)
31
+ ShopifyApp::Logger.debug("PostAuthenticateTasks: Performing after_authenticate_job")
32
+ config = ShopifyApp.configuration.after_authenticate_job
33
+
34
+ return unless config && config[:job].present?
35
+
36
+ job = config[:job]
37
+ job = job.constantize if job.is_a?(String)
38
+
39
+ if config[:inline] == true
40
+ job.perform_now(shop_domain: session.shop)
41
+ else
42
+ job.perform_later(shop_domain: session.shop)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module Auth
5
+ class TokenExchange
6
+ attr_reader :id_token
7
+
8
+ def self.perform(id_token)
9
+ new(id_token).perform
10
+ end
11
+
12
+ def initialize(id_token)
13
+ @id_token = id_token
14
+ end
15
+
16
+ def perform
17
+ domain = ShopifyAPI::Auth::JwtPayload.new(id_token).shopify_domain
18
+
19
+ Logger.info("Performing Token Exchange for [#{domain}] - (Offline)")
20
+ session = exchange_token(
21
+ shop: domain,
22
+ id_token: id_token,
23
+ requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
24
+ )
25
+
26
+ if online_token_configured?
27
+ Logger.info("Performing Token Exchange for [#{domain}] - (Online)")
28
+ session = exchange_token(
29
+ shop: domain,
30
+ id_token: id_token,
31
+ requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN,
32
+ )
33
+ end
34
+
35
+ ShopifyApp.configuration.post_authenticate_tasks.perform(session)
36
+
37
+ session
38
+ end
39
+
40
+ private
41
+
42
+ def exchange_token(shop:, id_token:, requested_token_type:)
43
+ session = ShopifyAPI::Auth::TokenExchange.exchange_token(
44
+ shop: shop,
45
+ session_token: id_token,
46
+ requested_token_type: requested_token_type,
47
+ )
48
+
49
+ SessionRepository.store_session(session)
50
+
51
+ session
52
+ rescue ShopifyAPI::Errors::InvalidJwtTokenError
53
+ Logger.error("Invalid id token '#{id_token}' during token exchange")
54
+ raise
55
+ rescue ShopifyAPI::Errors::HttpResponseError => error
56
+ Logger.error(
57
+ "A #{error.code} error (#{error.class}) occurred during the token exchange. Response: #{error.response.body}",
58
+ )
59
+ raise
60
+ rescue ActiveRecord::RecordNotUnique
61
+ Logger.debug("Session not stored due to concurrent token exchange calls")
62
+ session
63
+ rescue => error
64
+ Logger.error("An error occurred during the token exchange: [#{error.class}] #{error.message}")
65
+ raise
66
+ end
67
+
68
+ def online_token_configured?
69
+ ShopifyApp.configuration.online_token_configured?
70
+ end
71
+ end
72
+ end
73
+ end