shopify_app 21.2.0 → 21.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +7 -8
  3. data/.github/workflows/stale.yml +1 -0
  4. data/.spin/rails/prepare-application +8 -0
  5. data/CHANGELOG.md +15 -0
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +106 -91
  8. data/README.md +19 -15
  9. data/SECURITY.md +1 -1
  10. data/app/controllers/concerns/shopify_app/authenticated.rb +4 -9
  11. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +3 -2
  12. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +19 -0
  13. data/app/controllers/concerns/shopify_app/ensure_installed.rb +62 -0
  14. data/app/controllers/concerns/shopify_app/require_known_shop.rb +3 -38
  15. data/app/controllers/shopify_app/authenticated_controller.rb +1 -1
  16. data/app/controllers/shopify_app/callback_controller.rb +64 -27
  17. data/app/controllers/shopify_app/extension_verification_controller.rb +4 -1
  18. data/app/controllers/shopify_app/sessions_controller.rb +11 -2
  19. data/config/locales/ja.yml +1 -1
  20. data/docs/Troubleshooting.md +38 -2
  21. data/docs/Upgrading.md +40 -32
  22. data/docs/shopify_app/controller-concerns.md +48 -0
  23. data/docs/shopify_app/logging.md +21 -0
  24. data/docs/shopify_app/webhooks.md +13 -0
  25. data/lib/generators/shopify_app/add_app_uninstalled_job/add_app_uninstalled_job_generator.rb +15 -0
  26. data/lib/generators/shopify_app/add_app_uninstalled_job/templates/app_uninstalled_job.rb.tt +22 -0
  27. data/lib/generators/shopify_app/add_gdpr_jobs/add_gdpr_jobs_generator.rb +23 -0
  28. data/lib/generators/shopify_app/add_gdpr_jobs/templates/customers_data_request_job.rb.tt +22 -0
  29. data/lib/generators/shopify_app/add_gdpr_jobs/templates/customers_redact_job.rb.tt +22 -0
  30. data/lib/generators/shopify_app/add_gdpr_jobs/templates/shop_redact_job.rb.tt +22 -0
  31. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +1 -0
  32. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +2 -1
  33. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +2 -1
  34. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +1 -1
  35. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
  36. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -1
  37. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +8 -2
  38. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  39. data/lib/generators/shopify_app/shopify_app_generator.rb +2 -0
  40. data/lib/shopify_app/access_scopes/noop_strategy.rb +4 -0
  41. data/lib/shopify_app/access_scopes/user_strategy.rb +5 -0
  42. data/lib/shopify_app/configuration.rb +11 -0
  43. data/lib/shopify_app/controller_concerns/ensure_billing.rb +3 -0
  44. data/lib/shopify_app/controller_concerns/itp.rb +5 -0
  45. data/lib/shopify_app/controller_concerns/login_protection.rb +52 -13
  46. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +4 -1
  47. data/lib/shopify_app/controller_concerns/webhook_verification.rb +4 -1
  48. data/lib/shopify_app/logger.rb +28 -0
  49. data/lib/shopify_app/managers/scripttags_manager.rb +1 -0
  50. data/lib/shopify_app/managers/webhooks_manager.rb +6 -0
  51. data/lib/shopify_app/session/jwt.rb +1 -1
  52. data/lib/shopify_app/session/session_repository.rb +15 -4
  53. data/lib/shopify_app/version.rb +1 -1
  54. data/lib/shopify_app.rb +2 -0
  55. data/shopify_app.gemspec +2 -1
  56. data/yarn.lock +5 -5
  57. metadata +30 -4
@@ -3,46 +3,11 @@
3
3
  module ShopifyApp
4
4
  module RequireKnownShop
5
5
  extend ActiveSupport::Concern
6
- include ShopifyApp::RedirectForEmbedded
6
+ include ShopifyApp::EnsureInstalled
7
7
 
8
8
  included do
9
- before_action :check_shop_domain
10
- before_action :check_shop_known
11
- end
12
-
13
- def current_shopify_domain
14
- return if params[:shop].blank?
15
-
16
- @shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
17
- end
18
-
19
- private
20
-
21
- def check_shop_domain
22
- redirect_to(ShopifyApp.configuration.login_url) unless current_shopify_domain
23
- end
24
-
25
- def check_shop_known
26
- @shop = SessionRepository.retrieve_shop_session_by_shopify_domain(current_shopify_domain)
27
- unless @shop
28
- if embedded_param?
29
- redirect_for_embedded
30
- else
31
- redirect_to(shop_login)
32
- end
33
- end
34
- end
35
-
36
- def shop_login
37
- url = URI(ShopifyApp.configuration.login_url)
38
-
39
- url.query = URI.encode_www_form(
40
- shop: params[:shop],
41
- host: params[:host],
42
- return_to: request.fullpath,
43
- )
44
-
45
- url.to_s
9
+ ShopifyApp::Logger.deprecated("RequireKnownShop has been replaced by EnsureInstalled."\
10
+ " Please use the EnsureInstalled controller concern for the same behavior", "22.0.0")
46
11
  end
47
12
  end
48
13
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyApp
4
4
  class AuthenticatedController < ActionController::Base
5
- include ShopifyApp::Authenticated
5
+ include ShopifyApp::EnsureHasSession
6
6
 
7
7
  protect_from_forgery with: :exception
8
8
  end
@@ -8,49 +8,86 @@ module ShopifyApp
8
8
 
9
9
  def callback
10
10
  begin
11
- filtered_params = request.parameters.symbolize_keys.slice(:code, :shop, :timestamp, :state, :host, :hmac)
12
-
13
- auth_result = ShopifyAPI::Auth::Oauth.validate_auth_callback(
14
- cookies: {
15
- ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME =>
16
- cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME],
17
- },
18
- auth_query: ShopifyAPI::Auth::Oauth::AuthQuery.new(**filtered_params),
19
- )
20
- rescue
11
+ api_session, cookie = validated_auth_objects
12
+ rescue => error
13
+ deprecate_callback_rescue(error) unless error.class.module_parent == ShopifyAPI::Errors
21
14
  return respond_with_error
22
15
  end
23
16
 
24
- cookies.encrypted[auth_result[:cookie].name] = {
25
- expires: auth_result[:cookie].expires,
26
- secure: true,
27
- http_only: true,
28
- value: auth_result[:cookie].value,
29
- }
17
+ save_session(api_session) if api_session
18
+ update_rails_cookie(api_session, cookie)
30
19
 
31
- session[:shopify_user_id] = auth_result[:session].associated_user.id if auth_result[:session].online?
20
+ return respond_with_user_token_flow if start_user_token_flow?(api_session)
32
21
 
33
- if start_user_token_flow?(auth_result[:session])
34
- return respond_with_user_token_flow
35
- end
22
+ perform_post_authenticate_jobs(api_session)
23
+ redirect_to_app if check_billing(api_session)
24
+ end
36
25
 
37
- perform_post_authenticate_jobs(auth_result[:session])
38
- has_payment = check_billing(auth_result[:session])
26
+ private
39
27
 
40
- respond_successfully if has_payment
28
+ def deprecate_callback_rescue(error)
29
+ message = <<~EOS
30
+ An error of type #{error.class} was rescued. This is not part of `ShopifyAPI::Errors`, which could indicate a
31
+ bug in your app, or a bug in the shopify_app gem. Future versions of the gem may re-raise this error rather
32
+ than rescuing it.
33
+ EOS
34
+ ShopifyApp::Logger.deprecated(message, "22.0.0")
41
35
  end
42
36
 
43
- private
37
+ def save_session(api_session)
38
+ ShopifyApp::SessionRepository.store_session(api_session)
39
+ end
40
+
41
+ def validated_auth_objects
42
+ filtered_params = request.parameters.symbolize_keys.slice(:code, :shop, :timestamp, :state, :host, :hmac)
43
+
44
+ oauth_payload = ShopifyAPI::Auth::Oauth.validate_auth_callback(
45
+ cookies: {
46
+ ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME =>
47
+ cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME],
48
+ },
49
+ auth_query: ShopifyAPI::Auth::Oauth::AuthQuery.new(**filtered_params),
50
+ )
51
+ api_session = oauth_payload.dig(:session)
52
+ cookie = oauth_payload.dig(:cookie)
53
+
54
+ [api_session, cookie]
55
+ end
56
+
57
+ def update_rails_cookie(api_session, cookie)
58
+ if cookie.value.present?
59
+ cookies.encrypted[cookie.name] = {
60
+ expires: cookie.expires,
61
+ secure: true,
62
+ http_only: true,
63
+ value: cookie.value,
64
+ }
65
+ end
66
+
67
+ session[:shopify_user_id] = api_session.associated_user.id if api_session.online?
68
+ ShopifyApp::Logger.debug("Saving Shopify user ID to cookie")
69
+ end
44
70
 
45
- def respond_successfully
71
+ def redirect_to_app
46
72
  if ShopifyAPI::Context.embedded?
47
- return_to = session.delete(:return_to) || ""
48
- redirect_to(ShopifyAPI::Auth.embedded_app_url(params[:host]) + return_to, allow_other_host: true)
73
+ return_to = "#{decoded_host}#{session.delete(:return_to)}"
74
+ return_to = ShopifyApp.configuration.root_url if deduced_phishing_attack?
75
+ redirect_to(return_to, allow_other_host: true)
49
76
  else
50
77
  redirect_to(return_address)
51
78
  end
52
79
  end
53
80
 
81
+ def decoded_host
82
+ @decoded_host ||= ShopifyAPI::Auth.embedded_app_url(params[:host])
83
+ end
84
+
85
+ # host param doesn't match the configured myshopify_domain
86
+ def deduced_phishing_attack?
87
+ sanitized_host = ShopifyApp::Utils.sanitize_shop_domain(URI(decoded_host).host)
88
+ sanitized_host.nil?
89
+ end
90
+
54
91
  def respond_with_error
55
92
  flash[:error] = I18n.t("could_not_log_in")
56
93
  redirect_to(login_url_with_optional_shop)
@@ -9,7 +9,10 @@ module ShopifyApp
9
9
  private
10
10
 
11
11
  def verify_request
12
- head(:unauthorized) unless hmac_valid?(request.body.read)
12
+ unless hmac_valid?(request.body.read)
13
+ head(:unauthorized)
14
+ ShopifyApp::Logger.debug("Extension verification failed due to invalid HMAC")
15
+ end
13
16
  end
14
17
  end
15
18
  end
@@ -27,6 +27,8 @@ module ShopifyApp
27
27
  def destroy
28
28
  reset_session
29
29
  flash[:notice] = I18n.t(".logged_out")
30
+ ShopifyApp::Logger.debug("Session destroyed")
31
+ ShopifyApp::Logger.debug("Redirecting to #{login_url_with_optional_shop}")
30
32
  redirect_to(login_url_with_optional_shop)
31
33
  end
32
34
 
@@ -38,6 +40,7 @@ module ShopifyApp
38
40
  copy_return_to_param_to_session
39
41
 
40
42
  if embedded_redirect_url?
43
+ ShopifyApp::Logger.debug("Embedded URL within / authenticate")
41
44
  if embedded_param?
42
45
  redirect_for_embedded
43
46
  else
@@ -52,6 +55,7 @@ module ShopifyApp
52
55
 
53
56
  def start_oauth
54
57
  callback_url = ShopifyApp.configuration.login_callback_url.gsub(%r{^/}, "")
58
+ ShopifyApp::Logger.debug("Starting OAuth with the following callback URL: #{callback_url}")
55
59
 
56
60
  auth_attributes = ShopifyAPI::Auth::Oauth.begin_auth(
57
61
  shop: sanitized_shop_name,
@@ -65,7 +69,10 @@ module ShopifyApp
65
69
  value: auth_attributes[:cookie].value,
66
70
  }
67
71
 
68
- redirect_to(auth_attributes[:auth_route], allow_other_host: true)
72
+ auth_route = auth_attributes[:auth_route]
73
+
74
+ ShopifyApp::Logger.debug("Redirecting to auth_route - #{auth_route}")
75
+ redirect_to(auth_route, allow_other_host: true)
69
76
  end
70
77
 
71
78
  def validate_shop_presence
@@ -94,7 +101,9 @@ module ShopifyApp
94
101
  end
95
102
 
96
103
  def redirect_auth_to_top_level
97
- fullpage_redirect_to(login_url_with_optional_shop(top_level: true))
104
+ url = login_url_with_optional_shop(top_level: true)
105
+ ShopifyApp::Logger.debug("Redirecting to top level - #{url}")
106
+ fullpage_redirect_to(url)
98
107
  end
99
108
  end
100
109
  end
@@ -8,7 +8,7 @@ ja:
8
8
  enable_cookies_footer: Cookieを使用すると、各種設定や個人情報を一時的に保存することで、アプリ認証を受けることができます。30日後に有効期限が切れます。
9
9
  enable_cookies_action: Cookieを有効にする
10
10
  top_level_interaction_heading: お使いのブラウザを更新する必要があります%{app}
11
- top_level_interaction_body: Shopifyがアプリを開けるように、ブラウザーはCookieにアクセスするための%{app}のようなアプリが必要です。
11
+ top_level_interaction_body: Shopifyがアプリを開けるように、ブラウザはCookieにアクセスするための%{app}のようなアプリが必要です。
12
12
  top_level_interaction_action: 続ける
13
13
  request_storage_access_heading: "%{app}はCookieへのアクセス許可が必要です"
14
14
  request_storage_access_body: Cookieを使用すると、個人情報を一時的に保存することで、アプリ認証を受けることができます。[続ける]
@@ -3,7 +3,7 @@
3
3
  #### Table of contents
4
4
 
5
5
  [Generators](#generators)
6
- * [The `shopify_app:install` generator hangs](#the-shopifyappinstall-generator-hangs)
6
+ * [The `shopify_app:install` generator hangs](#the-shopify_appinstall-generator-hangs)
7
7
 
8
8
  [Rails](#rails)
9
9
  * [Known issues with Rails `v6.1`](#known-issues-with-rails-v61)
@@ -18,6 +18,8 @@
18
18
  * [My app can't make requests to the Shopify API](#my-app-cant-make-requests-to-the-shopify-api)
19
19
  * [I'm stuck in a redirect loop after OAuth](#im-stuck-in-a-redirect-loop-after-oauth)
20
20
 
21
+ [Debugging Tips](#debugging-tips)
22
+
21
23
  ## Generators
22
24
 
23
25
  ### The shopify_app:install generator hangs
@@ -143,9 +145,43 @@ X-Shopify-API-Request-Failure-Unauthorized: true
143
145
 
144
146
  Then, use the [Shopify App Bridge Redirect](https://shopify.dev/tools/app-bridge/actions/navigation/redirect) action to redirect your app frontend to the app login URL if this header is set.
145
147
 
146
-
147
148
  ### I'm stuck in a redirect loop after OAuth
148
149
 
149
150
  In previous versions of `ShopifyApp::Authenticated` controller concern, App Bridge embedded apps were able to include the `Authenticated` controller concern in the `HomeController` and other embedded controllers. This is no longer supported due to browsers blocking 3rd party cookies to increase privacy. App Bridge 3 is needed to handle all embedded sessions.
150
151
 
151
152
  For more details on how to handle embeded sessions, refer to [the session token documentation](https://shopify.dev/apps/auth/oauth/session-tokens).
153
+
154
+ ### `redirect_uri is not whitelisted`
155
+
156
+ * Ensure you have set the `HOST` environment variable to match your host's URL, e.g. `http://localhost:3000` or `https://my-host-name.trycloudflare.com`.
157
+ * Update the app's URL and whitelisted URLs in App Setup on https://partners.shopify.com
158
+
159
+ ### `This app can’t load due to an issue with browser cookies`
160
+
161
+ This can be caused by an infinite redirect due to a coding error
162
+ To investigate the cause, you can add a breakpoint or logging to the `rescue` clause of `ShopifyApp::CallbackController`.
163
+
164
+ One possible cause is that for XHR requests, the `Authenticated` concern should be used, rather than `RequireKnownShop`.
165
+ See below for further details.
166
+
167
+ ## Controller Concerns
168
+ ### Authenticated vs RequireKnownShop
169
+ The gem heavily relies on the `current_shopify_domain` helper to contextualize a request to a given Shopify shop. This helper is set in different and conflicting ways if the request is authenticated or not.
170
+
171
+ Because of these conflicting approaches the `Authenticated` (for use in authenticated requests) and `RequireKnownShop` (for use in unauthenticated requests) controller concerns must *never* be included within the same controller.
172
+
173
+ #### Authenticated Requests
174
+ For authenticated requests, use the [`Authenticated` controller concern](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/authenticated.rb). The `current_shopify_domain` is set from the JWT for these requests.
175
+
176
+ #### Unauthenticated Requests
177
+ For unauthenticated requests, use the [`RequireKnownShop` controller concern](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/require_known_shop.rb). The `current_shopify_domain` is set from the query string parameters that are passed.
178
+
179
+ ## Debugging Tips
180
+
181
+ If you do run into issues with the gem there are two useful techniques to apply: Adding log statements, and using an interactive debugger, such as `pry`.
182
+
183
+ You can temporarily add log statements or debugger calls to the `shopify_app` or `shopify-api-ruby` gems:
184
+ * You can modify a gem using [`bundle open`](https://boringrails.com/tips/bundle-open-debug-gems)
185
+ * Alternatively, you can your modify your `Gemfile` to use local locally checked out gems with the the [`path` option](https://bundler.io/man/gemfile.5.html).
186
+
187
+ Note that if you make changes to a gem, you will need to restart the app for the changes to be applied.
data/docs/Upgrading.md CHANGED
@@ -4,6 +4,12 @@ This file documents important changes needed to upgrade your app's Shopify App v
4
4
 
5
5
  #### Table of contents
6
6
 
7
+ [General Advice](#general-advice)
8
+
9
+ [Unreleased](#unreleased)
10
+
11
+ [Upgrading to `v20.3.0`](#upgrading-to-v2030)
12
+
7
13
  [Upgrading to `v20.2.0`](#upgrading-to-v2020)
8
14
 
9
15
  [Upgrading to `v20.1.0`](#upgrading-to-v2010)
@@ -20,7 +26,29 @@ This file documents important changes needed to upgrade your app's Shopify App v
20
26
 
21
27
  [Upgrading from `v8.6` to `v9.0.0`](#upgrading-from-v86-to-v900)
22
28
 
29
+ ## General Advice
30
+
31
+ Although we strive to make upgrades as smooth as possible, some effort may be required to stay up to date with the latest changes to `shopify_app`.
32
+
33
+ We strongly recommend you avoid 'monkeypatching' any existing code from `ShopifyApp`, e.g. by inheriting from `ShopifyApp` and then overriding particular methods. This can result in difficult upgrades. If your app does so, you will need to carefully check the gem's internal changes when upgrading.
34
+
35
+ If you need to upgrade by more than one major version (e.g. from v18 to v20), we recommend doing one at a time. Deploy each into production to help to detect problems earlier.
36
+
37
+ We also recommend the use of a staging site which matches your production environment as closely as possible.
38
+
39
+ If you do run into issues, we recommend looking at our [debugging tips.](https://github.com/Shopify/shopify_app/blob/main/docs/Troubleshooting.md#debugging-tips)
40
+
41
+ ## Upgrading to 21.3.0
42
+ The `Itp` controller concern has been removed from `LoginProtection` which is included by the `Authenticated` controller concern.
43
+ If any of your controllers are dependant on methods from `Itp` then you can include `ShopifyApp::Itp` directly.
44
+ You may notice a deprecation notice saying, `Itp will be removed in an upcoming version`.
45
+ This is because we intend on removing `Itp` completely in `v22.0.0`, but this will work in the meantime.
46
+
47
+ ## Upgrading to `v20.3.0`
48
+ Calling `LoginProtection#current_shopify_domain` will no longer raise an error if there is no active session. It will now return a nil value. The internal behavior of raising an error on OAuth redirect is still in place, however. If you were calling `current_shopify_domain` in authenticated actions and expecting an error if nil, you'll need to do a presence check and raise that error within your app.
49
+
23
50
  ## Upgrading to `v20.2.0`
51
+
24
52
  All custom errors defined inline within the `ShopifyApp` gem have been moved to `lib/shopify_app/errors.rb`.
25
53
 
26
54
  - If you rescue any errors defined in this gem, you will need to rename them to match their new namespacing.
@@ -36,8 +64,11 @@ Note that the following steps are *optional* and only apply to **embedded** appl
36
64
 
37
65
  ## Upgrading to `v19.0.0`
38
66
 
39
- This update moves API authentication logic from this gem to the [`shopify_api`](https://github.com/Shopify/shopify-api-ruby)
40
- gem.
67
+ There are several major changes in this release:
68
+
69
+ * A change of strategy regarding sessions: Due to security changes with browsers, support for cookie based sessions was dropped. JWT is now the only supported method for managing sessions.
70
+ * As part of that change, this update moves API authentication logic from this gem to the [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) gem.
71
+ * Previously the `shopify_api` gem relied on `ActiveResource`, an outdated library which was [removed](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d) from Rails in 2012. v10 of `shopify_api` has a replacement approach which aims to provide a similar syntax, but changes will be necessary.
41
72
 
42
73
  ### High-level process
43
74
 
@@ -48,18 +79,20 @@ gem.
48
79
  - Remove `allow_jwt_authentication=` and `allow_cookie_authentication=` invocations from
49
80
  `config/initializers/shopify_app.rb` as the decision logic for which authentication method to use is now handled
50
81
  internally by the `shopify_api` gem, using the `ShopifyAPI::Context.embedded_app` setting.
51
- - `v19.0.0` updates the `shopify_api` dependency to `10.0.0`. This version of `shopify_api` has breaking changes. See
52
- the documentation for addressing these breaking changes on GitHub [here](https://github.com/Shopify/shopify-api-ruby#breaking-change-notice-for-version-1000).
82
+ - [Follow the guidance for upgrading `shopify-api-ruby`](https://github.com/Shopify/shopify-api-ruby#breaking-change-notice-for-version-1000).
53
83
 
54
84
  ### Specific cases
55
85
 
56
- #### Shopify user id in session
86
+ #### Shopify user ID in session
57
87
 
58
88
  Previously, we set the entire app user object in the `session` object.
59
89
  As of v19, since we no longer save the app user to the session (but only the shopify user id), we now store it as `session[:shopify_user_id]`. Please make sure to update any references to that object.
60
90
 
61
91
  #### Webhook Jobs
62
92
 
93
+ It is assumed that you have an ActiveJob implementation configured for `perform_later`, e.g. Sidekiq.
94
+ Ensure your jobs inherit from `ApplicationJob` or `ActiveJob::Base`.
95
+
63
96
  Add a new `handle` method to existing webhook jobs to go through the updated `shopify_api` gem.
64
97
 
65
98
  ```ruby
@@ -95,32 +128,7 @@ Shopify API session, or `nil` if no such session is available.
95
128
 
96
129
  #### Setting up `ShopifyAPI::Context`
97
130
 
98
- The `shopify_app` initializer must configure the `ShopifyAPI::Context`. The Rails generator will
99
- generate a block in the `shopify_app` initializer. To do so manually, ensure the following is
100
- part of the `after_initialize` block in `shopify_app.rb`.
101
-
102
- ```ruby
103
- Rails.application.config.after_initialize do
104
- if ShopifyApp.configuration.api_key.present? && ShopifyApp.configuration.secret.present?
105
- ShopifyAPI::Context.setup(
106
- api_key: ShopifyApp.configuration.api_key,
107
- api_secret_key: ShopifyApp.configuration.secret,
108
- old_api_secret_key: ShopifyApp.configuration.old_secret,
109
- api_version: ShopifyApp.configuration.api_version,
110
- host_name: URI(ENV.fetch('HOST', '')).host || '',
111
- scope: ShopifyApp.configuration.scope,
112
- is_private: !ENV.fetch('SHOPIFY_APP_PRIVATE_SHOP', '').empty?,
113
- is_embedded: ShopifyApp.configuration.embedded_app,
114
- session_storage: ShopifyApp::SessionRepository,
115
- logger: Rails.logger,
116
- private_shop: ENV.fetch('SHOPIFY_APP_PRIVATE_SHOP', nil),
117
- user_agent_prefix: "ShopifyApp/#{ShopifyApp::VERSION}"
118
- )
119
-
120
- ShopifyApp::WebhooksManager.add_registrations
121
- end
122
- end
123
- ```
131
+ The `shopify_app` initializer must configure the `ShopifyAPI::Context`. The Rails generator will generate a block in the `shopify_app` initializer. To do so manually, you can refer to `after_initialize` block in the [template](https://github.com/Shopify/shopify_app/blob/main/lib/generators/shopify_app/install/templates/shopify_app.rb.tt).
124
132
 
125
133
  ## Upgrading to `v18.1.2`
126
134
 
@@ -128,7 +136,7 @@ Version 18.1.2 replaces the deprecated EASDK redirect with an App Bridge 2 redir
128
136
 
129
137
  ## Upgrading to `v17.2.0`
130
138
 
131
- ### Different SameSite cookie attribute behaviour
139
+ ### Different SameSite cookie attribute behavior
132
140
 
133
141
  To support Rails `v6.1`, the [`SameSiteCookieMiddleware`](/lib/shopify_app/middleware/same_site_cookie_middleware.rb) was updated to configure cookies to `SameSite=None` if the app is embedded. Before this release, cookies were configured to `SameSite=None` only if this attribute had not previously been set before.
134
142
 
@@ -0,0 +1,48 @@
1
+ # Controller Concerns
2
+
3
+ The following controller concerns are designed to be public and can be included in your controllers. Concerns defined in `lib/shopify_app/controller_concerns` are designed to be private and are not meant to be included directly into your controllers.
4
+
5
+ ## Authenticated
6
+ Designed for controllers that are designed to handle authenticated actions by ensuring there is a valid session for the request.
7
+
8
+ In addition to session management, this concern will also handle localization, CSRF protection, embedded app settings, and billing enforcement.
9
+
10
+ #### LoginProtection - Session Management
11
+ This concern will setup and teardown the session around the action. If the session cannot be setup for the requested shop the request will be redirected to login.
12
+
13
+ The concern will load sessions depending on your app's configuration:
14
+
15
+ **Embedded apps**
16
+
17
+ Cookies are not available for embedded apps because it loads in an iframe, so this concern will load the session from the request's `Authorization` header containing a session token, which can be set using [App Bridge](https://shopify.dev/apps/tools/app-bridge).
18
+
19
+ Learn more about [using `authenticatedFetch`](https://shopify.dev/apps/auth/oauth/session-tokens/getting-started#step-2-authenticate-your-requests) to create session tokens and authenticate your requests.
20
+
21
+ **Non-embedded apps**
22
+
23
+ Since cookies are available, the concern will load the session directly from them, so you can make regular `fetch` requests on your front-end.
24
+
25
+ #### Localization
26
+ I18n localization is saved to the session for consistent translations for the session.
27
+
28
+ #### CSRFProtection
29
+ Implements Rails' [protect_from_forgery](https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html#method-i-protect_from_forgery) unless a valid session token is found for the request.
30
+
31
+ #### EmbeddedApp
32
+ If your ShopifyApp configuration has the `embedded_app` config set to true, [P3P header](https://www.w3.org/P3P/) and [content security policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) are handled for you.
33
+
34
+ #### EnsureBilling
35
+ If billing is enabled for the app, the active payment for the session is queried and enforced if needed. If billing is required the user will be redirected to a page requesting payment.
36
+
37
+ ## EnsureAuthenticatedLinks
38
+ Designed to be more of a lightweight session concern specifically for XHR requests. Where `Authenticated` does far more than just session management, this concern will redirect to the splash page of the app if no active session was found.
39
+
40
+ ## RequireKnownShop
41
+ Designed to handle unauthenticated requests for *embedded apps*. If you are non-embedded app, we recommend using `Authenticated` concern instead of this one.
42
+
43
+ Rather than using the JWT to determine the requested shop of the request, the `shop` name param is taken from the query string that Shopify Admin provides.
44
+
45
+ If the shop session cannot be found for the provided `shop` in the query string, the request will be redirected to login or the `embedded_redirect_url`.
46
+
47
+ ## ShopAccessScopesVerification
48
+ If scopes for the session don't match configuration of scopes defined in `config/initializers/shopify_app.rb` the user will be redirected to login or the `embedded_redirect_url`
@@ -0,0 +1,21 @@
1
+ # Logging
2
+
3
+ ## Log Levels
4
+
5
+ There are four log levels with `error` being the most severe.
6
+
7
+ 1. Debug
8
+ 2. Info
9
+ 3. Warn
10
+ 4. Error
11
+
12
+ ## Configuration
13
+
14
+ The logging is controlled by the `log_level` configuration setting.
15
+ The default log level is `:info`.
16
+ You can disable all logs by setting this to `:off`.
17
+
18
+ ## Upgrading
19
+
20
+ For a newly-generated app, the `shopify_app` initializer will contain the `log_level` setting.
21
+ If you are upgrading from a previous version of the `shopify_app` gem then you will need to add this manually, otherwise it will default to `:info`.
@@ -3,6 +3,7 @@
3
3
  #### Table of contents
4
4
 
5
5
  [Manage webhooks using `ShopifyApp::WebhooksManager`](#manage-webhooks-using-shopifyappwebhooksmanager)
6
+ [Mandatory GDPR Webhooks](#mandatory-gdpr-webhooks)
6
7
 
7
8
  ## Manage webhooks using `ShopifyApp::WebhooksManager`
8
9
 
@@ -70,3 +71,15 @@ rails g shopify_app:add_webhook --topic carts/update --path webhooks/carts_updat
70
71
  ```
71
72
 
72
73
  Where `--topic` is the topic and `--path` is the path the webhook should be sent to.
74
+
75
+ ## Mandatory GDPR Webhooks
76
+
77
+ We have three mandatory GDPR webhooks
78
+
79
+ 1. `customers/data_request`
80
+ 2. `customer/redact`
81
+ 3. `shop/redact`
82
+
83
+ The `generate shopify_app` command generated three job templates corresponding to all three of these webhooks.
84
+ To pass our approval process you will need to set these webhooks in your partner dashboard.
85
+ You can read more about that [here](https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks).
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class AddAppUninstalledJobGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def create_job
11
+ template("app_uninstalled_job.rb", "app/jobs/app_uninstalled_job.rb")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class AppUninstalledJob < 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
+ logger.info("#{self.class} started for shop '#{shop_domain}'")
20
+ shop.destroy
21
+ end
22
+ 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 AddGdprJobsGenerator < 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