shopify_app 21.2.0 → 21.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +7 -8
- data/.github/workflows/stale.yml +1 -0
- data/.spin/rails/prepare-application +8 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +106 -91
- data/README.md +19 -15
- data/SECURITY.md +1 -1
- data/app/controllers/concerns/shopify_app/authenticated.rb +4 -9
- data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +3 -2
- data/app/controllers/concerns/shopify_app/ensure_has_session.rb +19 -0
- data/app/controllers/concerns/shopify_app/ensure_installed.rb +62 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +3 -38
- data/app/controllers/shopify_app/authenticated_controller.rb +1 -1
- data/app/controllers/shopify_app/callback_controller.rb +64 -27
- data/app/controllers/shopify_app/extension_verification_controller.rb +4 -1
- data/app/controllers/shopify_app/sessions_controller.rb +11 -2
- data/config/locales/ja.yml +1 -1
- data/docs/Troubleshooting.md +38 -2
- data/docs/Upgrading.md +40 -32
- data/docs/shopify_app/controller-concerns.md +48 -0
- data/docs/shopify_app/logging.md +21 -0
- data/docs/shopify_app/webhooks.md +13 -0
- data/lib/generators/shopify_app/add_app_uninstalled_job/add_app_uninstalled_job_generator.rb +15 -0
- data/lib/generators/shopify_app/add_app_uninstalled_job/templates/app_uninstalled_job.rb.tt +22 -0
- data/lib/generators/shopify_app/add_gdpr_jobs/add_gdpr_jobs_generator.rb +23 -0
- data/lib/generators/shopify_app/add_gdpr_jobs/templates/customers_data_request_job.rb.tt +22 -0
- data/lib/generators/shopify_app/add_gdpr_jobs/templates/customers_redact_job.rb.tt +22 -0
- data/lib/generators/shopify_app/add_gdpr_jobs/templates/shop_redact_job.rb.tt +22 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +1 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +2 -1
- data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +2 -1
- data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +1 -1
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +8 -2
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
- data/lib/generators/shopify_app/shopify_app_generator.rb +2 -0
- data/lib/shopify_app/access_scopes/noop_strategy.rb +4 -0
- data/lib/shopify_app/access_scopes/user_strategy.rb +5 -0
- data/lib/shopify_app/configuration.rb +11 -0
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +3 -0
- data/lib/shopify_app/controller_concerns/itp.rb +5 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +52 -13
- data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +4 -1
- data/lib/shopify_app/controller_concerns/webhook_verification.rb +4 -1
- data/lib/shopify_app/logger.rb +28 -0
- data/lib/shopify_app/managers/scripttags_manager.rb +1 -0
- data/lib/shopify_app/managers/webhooks_manager.rb +6 -0
- data/lib/shopify_app/session/jwt.rb +1 -1
- data/lib/shopify_app/session/session_repository.rb +15 -4
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app.rb +2 -0
- data/shopify_app.gemspec +2 -1
- data/yarn.lock +5 -5
- metadata +30 -4
@@ -3,46 +3,11 @@
|
|
3
3
|
module ShopifyApp
|
4
4
|
module RequireKnownShop
|
5
5
|
extend ActiveSupport::Concern
|
6
|
-
include ShopifyApp::
|
6
|
+
include ShopifyApp::EnsureInstalled
|
7
7
|
|
8
8
|
included do
|
9
|
-
|
10
|
-
|
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
|
@@ -8,49 +8,86 @@ module ShopifyApp
|
|
8
8
|
|
9
9
|
def callback
|
10
10
|
begin
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
20
|
+
return respond_with_user_token_flow if start_user_token_flow?(api_session)
|
32
21
|
|
33
|
-
|
34
|
-
|
35
|
-
|
22
|
+
perform_post_authenticate_jobs(api_session)
|
23
|
+
redirect_to_app if check_billing(api_session)
|
24
|
+
end
|
36
25
|
|
37
|
-
|
38
|
-
has_payment = check_billing(auth_result[:session])
|
26
|
+
private
|
39
27
|
|
40
|
-
|
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
|
-
|
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
|
71
|
+
def redirect_to_app
|
46
72
|
if ShopifyAPI::Context.embedded?
|
47
|
-
return_to = session.delete(:return_to)
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/config/locales/ja.yml
CHANGED
@@ -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
|
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を使用すると、個人情報を一時的に保存することで、アプリ認証を受けることができます。[続ける]
|
data/docs/Troubleshooting.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#### Table of contents
|
4
4
|
|
5
5
|
[Generators](#generators)
|
6
|
-
* [The `shopify_app:install` generator hangs](#the-
|
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
|
-
|
40
|
-
|
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
|
-
-
|
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
|
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
|
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
|