shopify_app 21.6.0 → 22.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/bug-report.md +23 -18
  4. data/.github/workflows/build.yml +2 -2
  5. data/.github/workflows/release.yml +1 -1
  6. data/.github/workflows/rubocop.yml +1 -2
  7. data/.nvmrc +1 -1
  8. data/.rubocop.yml +0 -1
  9. data/CHANGELOG.md +115 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +1 -6
  12. data/Gemfile.lock +99 -96
  13. data/README.md +47 -2
  14. data/app/assets/javascripts/shopify_app/redirect.js +3 -10
  15. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +5 -1
  16. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +11 -5
  17. data/app/controllers/concerns/shopify_app/ensure_installed.rb +10 -4
  18. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +5 -1
  19. data/app/controllers/shopify_app/callback_controller.rb +39 -18
  20. data/app/controllers/shopify_app/sessions_controller.rb +25 -4
  21. data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
  22. data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
  23. data/app/views/shopify_app/shared/redirect.html.erb +10 -1
  24. data/config/locales/cs.yml +0 -18
  25. data/config/locales/da.yml +0 -15
  26. data/config/locales/de.yml +0 -17
  27. data/config/locales/en.yml +0 -11
  28. data/config/locales/es.yml +0 -17
  29. data/config/locales/fi.yml +0 -15
  30. data/config/locales/fr.yml +0 -18
  31. data/config/locales/it.yml +0 -16
  32. data/config/locales/ja.yml +0 -12
  33. data/config/locales/ko.yml +0 -14
  34. data/config/locales/nb.yml +0 -16
  35. data/config/locales/nl.yml +0 -16
  36. data/config/locales/pl.yml +0 -16
  37. data/config/locales/pt-BR.yml +0 -16
  38. data/config/locales/pt-PT.yml +0 -17
  39. data/config/locales/sv.yml +0 -16
  40. data/config/locales/th.yml +0 -15
  41. data/config/locales/tr.yml +0 -17
  42. data/config/locales/vi.yml +0 -17
  43. data/config/locales/zh-CN.yml +0 -11
  44. data/config/locales/zh-TW.yml +0 -11
  45. data/config/routes.rb +2 -1
  46. data/docs/Quickstart.md +9 -2
  47. data/docs/Troubleshooting.md +0 -23
  48. data/docs/Upgrading.md +64 -1
  49. data/docs/shopify_app/authentication.md +179 -58
  50. data/docs/shopify_app/controller-concerns.md +53 -12
  51. data/docs/shopify_app/generators.md +2 -2
  52. data/docs/shopify_app/sessions.md +358 -0
  53. data/docs/shopify_app/webhooks.md +88 -11
  54. data/karma.conf.js +6 -4
  55. data/lib/generators/shopify_app/add_declarative_webhook/add_declarative_webhook_generator.rb +53 -0
  56. data/lib/generators/shopify_app/add_declarative_webhook/templates/webhook_controller.rb.tt +13 -0
  57. data/lib/generators/shopify_app/add_declarative_webhook/templates/webhook_job.rb.tt +15 -0
  58. data/lib/generators/shopify_app/{add_gdpr_jobs/add_gdpr_jobs_generator.rb → add_privacy_jobs/add_privacy_jobs_generator.rb} +1 -1
  59. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +6 -1
  60. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +1 -0
  61. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +5 -2
  62. data/lib/generators/shopify_app/shopify_app_generator.rb +1 -1
  63. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_expires_at_column.erb +5 -0
  64. data/lib/generators/shopify_app/user_model/user_model_generator.rb +20 -0
  65. data/lib/shopify_app/admin_api/with_token_refetch.rb +27 -0
  66. data/lib/shopify_app/auth/post_authenticate_tasks.rb +48 -0
  67. data/lib/shopify_app/auth/token_exchange.rb +73 -0
  68. data/lib/shopify_app/configuration.rb +69 -1
  69. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +1 -1
  70. data/lib/shopify_app/controller_concerns/csrf_protection.rb +2 -1
  71. data/lib/shopify_app/controller_concerns/embedded_app.rb +42 -3
  72. data/lib/shopify_app/controller_concerns/ensure_billing.rb +14 -3
  73. data/lib/shopify_app/controller_concerns/frame_ancestors.rb +1 -1
  74. data/lib/shopify_app/controller_concerns/localization.rb +11 -8
  75. data/lib/shopify_app/controller_concerns/login_protection.rb +34 -38
  76. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +5 -0
  77. data/lib/shopify_app/controller_concerns/sanitized_params.rb +4 -0
  78. data/lib/shopify_app/controller_concerns/token_exchange.rb +111 -0
  79. data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +48 -0
  80. data/lib/shopify_app/engine.rb +5 -11
  81. data/lib/shopify_app/managers/webhooks_manager.rb +6 -2
  82. data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
  83. data/lib/shopify_app/session/in_memory_user_session_store.rb +1 -1
  84. data/lib/shopify_app/session/jwt.rb +9 -0
  85. data/lib/shopify_app/session/session_repository.rb +49 -8
  86. data/lib/shopify_app/session/shop_session_storage.rb +4 -0
  87. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +4 -0
  88. data/lib/shopify_app/session/user_session_storage.rb +4 -0
  89. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +25 -0
  90. data/lib/shopify_app/test_helpers/shopify_session_helper.rb +1 -0
  91. data/lib/shopify_app/utils.rb +14 -1
  92. data/lib/shopify_app/version.rb +1 -1
  93. data/lib/shopify_app.rb +9 -3
  94. data/package.json +5 -6
  95. data/shopify_app.gemspec +4 -4
  96. data/yarn.lock +2134 -3905
  97. metadata +51 -60
  98. data/.github/workflows/stale.yml +0 -43
  99. data/app/assets/images/storage_access.svg +0 -1
  100. data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +0 -10
  101. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +0 -22
  102. data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +0 -1
  103. data/app/controllers/concerns/shopify_app/authenticated.rb +0 -17
  104. data/app/controllers/concerns/shopify_app/require_known_shop.rb +0 -16
  105. data/docs/shopify_app/script-tags.md +0 -28
  106. data/docs/shopify_app/session-repository.md +0 -79
  107. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +0 -42
  108. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +0 -63
  109. data/lib/shopify_app/controller_concerns/itp.rb +0 -50
  110. data/lib/shopify_app/jobs/scripttags_manager_job.rb +0 -16
  111. data/lib/shopify_app/managers/scripttags_manager.rb +0 -85
  112. /data/lib/generators/shopify_app/{add_gdpr_jobs → add_privacy_jobs}/templates/customers_data_request_job.rb.tt +0 -0
  113. /data/lib/generators/shopify_app/{add_gdpr_jobs → add_privacy_jobs}/templates/customers_redact_job.rb.tt +0 -0
  114. /data/lib/generators/shopify_app/{add_gdpr_jobs → add_privacy_jobs}/templates/shop_redact_job.rb.tt +0 -0
@@ -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
@@ -4,7 +4,7 @@ require "rails/generators/base"
4
4
 
5
5
  module ShopifyApp
6
6
  module Generators
7
- class AddGdprJobsGenerator < Rails::Generators::Base
7
+ class AddPrivacyJobsGenerator < Rails::Generators::Base
8
8
  source_root File.expand_path("../templates", __FILE__)
9
9
 
10
10
  def add_customer_data_request_job
@@ -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
@@ -17,6 +17,7 @@ class <%= @job_class_name %> < ActiveJob::Base
17
17
  end
18
18
 
19
19
  shop.with_shopify_session do |session|
20
+ ## webhook processing logic
20
21
  end
21
22
  end
22
23
  end
@@ -4,6 +4,8 @@ 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'
@@ -12,7 +14,7 @@ ShopifyApp.configure do |config|
12
14
  config.webhooks = [
13
15
  { topic: "app/uninstalled", address: "webhooks/app_uninstalled"},
14
16
  { topic: "customers/data_request", address: "webhooks/customers_data_request" },
15
- { topic: "customer/redact", address: "webhooks/customers_redact"},
17
+ { topic: "customers/redact", address: "webhooks/customers_redact"},
16
18
  { topic: "shop/redact", address: "webhooks/shop_redact"}
17
19
  ]
18
20
 
@@ -30,7 +32,8 @@ ShopifyApp.configure do |config|
30
32
  # amount: 5,
31
33
  # interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS,
32
34
  # currency_code: "USD", # Only supports USD for now
33
- # test: ENV.fetch('SHOPIFY_TEST_CHARGES', !Rails.env.production?)
35
+ # trial_days: 0,
36
+ # test: !ENV['SHOPIFY_TEST_CHARGES'].nil? ? ["true", "1"].include?(ENV['SHOPIFY_TEST_CHARGES']) : !Rails.env.production?
34
37
  # )
35
38
 
36
39
  if defined? Rails::Server
@@ -10,7 +10,7 @@ module ShopifyApp
10
10
 
11
11
  def run_all_generators
12
12
  generate("shopify_app:add_app_uninstalled_job")
13
- generate("shopify_app:add_gdpr_jobs")
13
+ generate("shopify_app:add_privacy_jobs")
14
14
  generate("shopify_app:install #{@opts.join(" ")}")
15
15
  generate("shopify_app:shop_model #{@opts.join(" ")}")
16
16
  generate("shopify_app:authenticated_controller")
@@ -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
@@ -40,6 +40,26 @@ module ShopifyApp
40
40
  end
41
41
  end
42
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",
59
+ )
60
+ end
61
+ end
62
+
43
63
  def update_shopify_app_initializer
44
64
  gsub_file("config/initializers/shopify_app.rb", "ShopifyApp::InMemoryUserSessionStore", "User")
45
65
  end
@@ -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
@@ -20,6 +20,7 @@ module ShopifyApp
20
20
  attr_accessor :api_version
21
21
 
22
22
  attr_accessor :reauth_on_access_scope_changes
23
+ attr_accessor :check_session_expiry_date
23
24
  attr_accessor :log_level
24
25
 
25
26
  # customise urls
@@ -28,6 +29,9 @@ module ShopifyApp
28
29
  attr_writer :login_callback_url
29
30
  attr_accessor :embedded_redirect_url
30
31
 
32
+ # customize post authenticate tasks
33
+ attr_accessor :custom_post_authenticate_tasks
34
+
31
35
  # customise ActiveJob queue names
32
36
  attr_accessor :scripttags_manager_queue_name
33
37
  attr_accessor :webhooks_manager_queue_name
@@ -35,6 +39,9 @@ module ShopifyApp
35
39
  # configure myshopify domain for local shopify development
36
40
  attr_accessor :myshopify_domain
37
41
 
42
+ # configure the unified admin domain for local shopify development
43
+ attr_accessor :unified_admin_domain
44
+
38
45
  # ability to have webpacker installed but not used in this gem and the generators
39
46
  attr_accessor :disable_webpacker
40
47
 
@@ -44,12 +51,19 @@ module ShopifyApp
44
51
  # takes a ShopifyApp::BillingConfiguration object
45
52
  attr_accessor :billing
46
53
 
54
+ # Enables new authorization flow using token exchange
55
+ attr_accessor :new_embedded_auth_strategy
56
+
47
57
  def initialize
48
58
  @root_url = "/"
49
59
  @myshopify_domain = "myshopify.com"
60
+ @unified_admin_domain = "shopify.com"
50
61
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
51
62
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
52
63
  @disable_webpacker = ENV["SHOPIFY_APP_DISABLE_WEBPACKER"].present?
64
+ @scope = []
65
+
66
+ log_v23_deprecations
53
67
  end
54
68
 
55
69
  def login_url
@@ -118,6 +132,58 @@ module ShopifyApp
118
132
  def user_access_scopes
119
133
  @user_access_scopes || scope
120
134
  end
135
+
136
+ def use_new_embedded_auth_strategy?
137
+ new_embedded_auth_strategy && embedded_app?
138
+ end
139
+
140
+ def online_token_configured?
141
+ !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
142
+ end
143
+
144
+ def post_authenticate_tasks
145
+ @post_authenticate_tasks || begin
146
+ if custom_post_authenticate_tasks
147
+ custom_class = if custom_post_authenticate_tasks.respond_to?(:safe_constantize)
148
+ custom_post_authenticate_tasks.safe_constantize
149
+ else
150
+ custom_post_authenticate_tasks
151
+ end
152
+ end
153
+
154
+ task_class = custom_class || ShopifyApp::Auth::PostAuthenticateTasks
155
+
156
+ [
157
+ :perform,
158
+ ].each do |method|
159
+ raise(
160
+ ::ShopifyApp::ConfigurationError,
161
+ "Missing method - '#{method}' for custom_post_authenticate_tasks",
162
+ ) unless task_class.respond_to?(method)
163
+ end
164
+
165
+ task_class
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ def log_v23_deprecations
172
+ return unless Rails.env.development?
173
+
174
+ # TODO: Remove this before releasing v23.0.0
175
+ message = <<~EOS
176
+ ================================================
177
+ => Upcoming changes in v23.0:
178
+ * 'CallbackController::perform_after_authenticate_job' and related methods 'install_webhooks', 'perform_after_authenticate_job'
179
+ * are deprecated and will be removed from CallbackController in the next major release. If you need to customize
180
+ * post authentication tasks, see https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/authentication.md#post-authenticate-tasks
181
+
182
+ * ShopifyApp::JWTMiddleware will be removed, use ShopifyApp::WithShopifyIdToken instead.
183
+ ================================================
184
+ EOS
185
+ puts message
186
+ end
121
187
  end
122
188
 
123
189
  class BillingConfiguration
@@ -129,13 +195,15 @@ module ShopifyApp
129
195
  attr_reader :amount
130
196
  attr_reader :currency_code
131
197
  attr_reader :interval
198
+ attr_reader :trial_days
132
199
  attr_reader :test
133
200
 
134
- def initialize(charge_name:, amount:, interval:, currency_code: "USD", test: !Rails.env.production?)
201
+ def initialize(charge_name:, amount:, interval:, currency_code: "USD", trial_days: 0, test: !Rails.env.production?)
135
202
  @charge_name = charge_name
136
203
  @amount = amount
137
204
  @currency_code = currency_code
138
205
  @interval = interval
206
+ @trial_days = trial_days
139
207
  @test = test
140
208
  end
141
209
  end
@@ -9,7 +9,7 @@ module ShopifyApp
9
9
  end
10
10
 
11
11
  def verify_proxy_request
12
- return head(:forbidden) unless query_string_valid?(request.query_string)
12
+ head(:forbidden) unless query_string_valid?(request.query_string)
13
13
  end
14
14
 
15
15
  private
@@ -4,13 +4,14 @@ module ShopifyApp
4
4
  module CsrfProtection
5
5
  extend ActiveSupport::Concern
6
6
  included do
7
+ include ShopifyApp::WithShopifyIdToken
7
8
  protect_from_forgery with: :exception, unless: :valid_session_token?
8
9
  end
9
10
 
10
11
  private
11
12
 
12
13
  def valid_session_token?
13
- request.env["jwt.shopify_domain"]
14
+ jwt_payload.present?
14
15
  end
15
16
  end
16
17
  end
@@ -5,19 +5,58 @@ module ShopifyApp
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  include ShopifyApp::FrameAncestors
8
+ include ShopifyApp::SanitizedParams
8
9
 
9
10
  included do
10
- if ShopifyApp.configuration.embedded_app?
11
- after_action(:set_esdk_headers)
12
- layout("embedded_app")
11
+ layout :embedded_app_layout
12
+ after_action :set_esdk_headers, if: -> { ShopifyApp.configuration.embedded_app? }
13
+ end
14
+
15
+ protected
16
+
17
+ def redirect_to_embed_app_in_admin
18
+ ShopifyApp::Logger.debug("Redirecting to embed app in admin")
19
+
20
+ host = if params[:host]
21
+ params[:host]
22
+ elsif params[:shop]
23
+ Base64.encode64("#{sanitized_shop_name}/admin")
24
+ else
25
+ return redirect_to(ShopifyApp.configuration.login_url)
13
26
  end
27
+
28
+ original_path = request.path
29
+ original_params = request.query_parameters.except(:host, :shop, :id_token)
30
+ original_path += "?#{original_params.to_query}" if original_params.present?
31
+
32
+ redirect_path = ShopifyAPI::Auth.embedded_app_url(host) + original_path.to_s
33
+ redirect_path = ShopifyApp.configuration.root_url if deduced_phishing_attack?(redirect_path)
34
+ redirect_to(redirect_path, allow_other_host: true)
35
+ end
36
+
37
+ def use_embedded_app_layout?
38
+ ShopifyApp.configuration.embedded_app?
14
39
  end
15
40
 
16
41
  private
17
42
 
43
+ def embedded_app_layout
44
+ "embedded_app" if use_embedded_app_layout?
45
+ end
46
+
18
47
  def set_esdk_headers
19
48
  response.set_header("P3P", 'CP="Not used"')
20
49
  response.headers.except!("X-Frame-Options")
21
50
  end
51
+
52
+ def deduced_phishing_attack?(decoded_host)
53
+ sanitized_host = ShopifyApp::Utils.sanitize_shop_domain(decoded_host)
54
+ if sanitized_host.nil?
55
+ message = "Host param for redirect to embed app in admin is not from a trusted domain, " \
56
+ "redirecting to root as this is likely a phishing attack."
57
+ ShopifyApp::Logger.info(message)
58
+ end
59
+ sanitized_host.nil?
60
+ end
22
61
  end
23
62
  end
@@ -27,7 +27,7 @@ module ShopifyApp
27
27
 
28
28
  unless has_payment
29
29
  if request.xhr?
30
- add_top_level_redirection_headers(url: confirmation_url, ignore_response_code: true)
30
+ RedirectForEmbedded.add_app_bridge_redirect_url_header(confirmation_url, response)
31
31
  ShopifyApp::Logger.debug("Responding with 401 unauthorized")
32
32
  head(:unauthorized)
33
33
  elsif ShopifyApp.configuration.embedded_app?
@@ -45,8 +45,16 @@ module ShopifyApp
45
45
  end
46
46
 
47
47
  def handle_billing_error(error)
48
- logger.info("#{error.message}: #{error.errors}")
49
- redirect_to_login
48
+ ShopifyApp::Logger.warn("Encountered billing error - #{error.message}: #{error.errors}\n" \
49
+ "Redirecting to login page")
50
+
51
+ login_url = ShopifyApp.configuration.login_url
52
+ if request.xhr?
53
+ RedirectForEmbedded.add_app_bridge_redirect_url_header(login_url, response)
54
+ head(:unauthorized)
55
+ else
56
+ fullpage_redirect_to(login_url)
57
+ end
50
58
  end
51
59
 
52
60
  def has_active_payment?(session)
@@ -136,6 +144,7 @@ module ShopifyApp
136
144
  },
137
145
  },
138
146
  returnUrl: return_url,
147
+ trialDays: ShopifyApp.configuration.billing.trial_days,
139
148
  test: ShopifyApp.configuration.billing.test,
140
149
  },
141
150
  )
@@ -208,12 +217,14 @@ module ShopifyApp
208
217
  $name: String!
209
218
  $lineItems: [AppSubscriptionLineItemInput!]!
210
219
  $returnUrl: URL!
220
+ $trialDays: Int
211
221
  $test: Boolean
212
222
  ) {
213
223
  appSubscriptionCreate(
214
224
  name: $name
215
225
  lineItems: $lineItems
216
226
  returnUrl: $returnUrl
227
+ trialDays: $trialDays
217
228
  test: $test
218
229
  ) {
219
230
  confirmationUrl
@@ -8,7 +8,7 @@ module ShopifyApp
8
8
  content_security_policy do |policy|
9
9
  policy.frame_ancestors(-> do
10
10
  domain_host = current_shopify_domain || "*.#{::ShopifyApp.configuration.myshopify_domain}"
11
- "#{ShopifyAPI::Context.host_scheme}://#{domain_host} https://admin.shopify.com"
11
+ "#{ShopifyAPI::Context.host_scheme}://#{domain_host} https://admin.#{::ShopifyApp.configuration.unified_admin_domain}"
12
12
  end)
13
13
  end
14
14
  end
@@ -5,20 +5,23 @@ module ShopifyApp
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- before_action :set_locale
8
+ around_action :set_locale
9
9
  end
10
10
 
11
11
  private
12
12
 
13
- def set_locale
14
- if params[:locale]
15
- session[:locale] = params[:locale]
16
- else
17
- session[:locale] ||= I18n.default_locale
13
+ def set_locale(&action)
14
+ locale = params[:locale] || session[:locale] || I18n.default_locale
15
+
16
+ # Fallback to the 2 letter language code if the requested locale unavailable
17
+ unless I18n.available_locales.include?(locale.to_sym)
18
+ locale = locale.split("-").first
18
19
  end
19
- I18n.locale = session[:locale]
20
+
21
+ session[:locale] = locale
22
+ I18n.with_locale(session[:locale], &action)
20
23
  rescue I18n::InvalidLocale
21
- I18n.locale = I18n.default_locale
24
+ I18n.with_locale(I18n.default_locale, &action)
22
25
  end
23
26
  end
24
27
  end