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.
- 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
|