shopify_app 21.0.0 → 22.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/ENHANCEMENT.md +9 -0
  4. data/.github/ISSUE_TEMPLATE/bug-report.md +30 -47
  5. data/.github/ISSUE_TEMPLATE/feature-request.md +5 -29
  6. data/.github/workflows/build.yml +11 -12
  7. data/.github/workflows/release.yml +2 -2
  8. data/.github/workflows/remove-labels-on-activity.yml +1 -1
  9. data/.github/workflows/rubocop.yml +2 -3
  10. data/.nvmrc +1 -1
  11. data/.rubocop.yml +2 -1
  12. data/.ruby-version +1 -1
  13. data/.spin/rails/prepare-application +8 -0
  14. data/CHANGELOG.md +173 -7
  15. data/CODE_OF_CONDUCT.md +46 -0
  16. data/CONTRIBUTING.md +16 -6
  17. data/Gemfile +1 -0
  18. data/Gemfile.lock +160 -121
  19. data/README.md +67 -19
  20. data/SECURITY.md +1 -1
  21. data/app/assets/javascripts/shopify_app/redirect.js +3 -10
  22. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +9 -4
  23. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +25 -0
  24. data/app/controllers/concerns/shopify_app/ensure_installed.rb +84 -0
  25. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +5 -1
  26. data/app/controllers/shopify_app/authenticated_controller.rb +1 -1
  27. data/app/controllers/shopify_app/callback_controller.rb +101 -39
  28. data/app/controllers/shopify_app/extension_verification_controller.rb +4 -1
  29. data/app/controllers/shopify_app/sessions_controller.rb +37 -7
  30. data/app/controllers/shopify_app/webhooks_controller.rb +1 -1
  31. data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
  32. data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
  33. data/app/views/shopify_app/shared/redirect.html.erb +10 -1
  34. data/config/locales/cs.yml +0 -18
  35. data/config/locales/da.yml +0 -15
  36. data/config/locales/de.yml +0 -17
  37. data/config/locales/en.yml +0 -11
  38. data/config/locales/es.yml +0 -17
  39. data/config/locales/fi.yml +0 -15
  40. data/config/locales/fr.yml +0 -18
  41. data/config/locales/it.yml +0 -16
  42. data/config/locales/ja.yml +0 -12
  43. data/config/locales/ko.yml +0 -14
  44. data/config/locales/nb.yml +0 -16
  45. data/config/locales/nl.yml +0 -16
  46. data/config/locales/pl.yml +0 -16
  47. data/config/locales/pt-BR.yml +0 -16
  48. data/config/locales/pt-PT.yml +0 -17
  49. data/config/locales/sv.yml +0 -16
  50. data/config/locales/th.yml +0 -15
  51. data/config/locales/tr.yml +0 -17
  52. data/config/locales/vi.yml +0 -17
  53. data/config/locales/zh-CN.yml +0 -11
  54. data/config/locales/zh-TW.yml +0 -11
  55. data/config/routes.rb +2 -1
  56. data/docs/Quickstart.md +14 -5
  57. data/docs/Troubleshooting.md +38 -25
  58. data/docs/Upgrading.md +103 -32
  59. data/docs/shopify_app/authentication.md +179 -58
  60. data/docs/shopify_app/controller-concerns.md +89 -0
  61. data/docs/shopify_app/engine.md +2 -11
  62. data/docs/shopify_app/generators.md +2 -2
  63. data/docs/shopify_app/logging.md +21 -0
  64. data/docs/shopify_app/sessions.md +358 -0
  65. data/docs/shopify_app/testing.md +32 -10
  66. data/docs/shopify_app/webhooks.md +97 -7
  67. data/karma.conf.js +6 -4
  68. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +6 -3
  69. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -1
  70. data/lib/generators/shopify_app/add_app_uninstalled_job/add_app_uninstalled_job_generator.rb +15 -0
  71. data/lib/generators/shopify_app/add_app_uninstalled_job/templates/app_uninstalled_job.rb.tt +22 -0
  72. data/lib/generators/shopify_app/add_declarative_webhook/add_declarative_webhook_generator.rb +53 -0
  73. data/lib/generators/shopify_app/add_declarative_webhook/templates/webhook_controller.rb.tt +13 -0
  74. data/lib/generators/shopify_app/add_declarative_webhook/templates/webhook_job.rb.tt +15 -0
  75. data/lib/generators/shopify_app/add_privacy_jobs/add_privacy_jobs_generator.rb +23 -0
  76. data/lib/generators/shopify_app/add_privacy_jobs/templates/customers_data_request_job.rb.tt +22 -0
  77. data/lib/generators/shopify_app/add_privacy_jobs/templates/customers_redact_job.rb.tt +22 -0
  78. data/lib/generators/shopify_app/add_privacy_jobs/templates/shop_redact_job.rb.tt +22 -0
  79. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +8 -3
  80. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +4 -2
  81. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +1 -1
  82. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +1 -1
  83. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
  84. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -1
  85. data/lib/generators/shopify_app/install/install_generator.rb +4 -4
  86. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +13 -3
  87. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -1
  88. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  89. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -1
  90. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +1 -1
  91. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +1 -1
  92. data/lib/generators/shopify_app/shopify_app_generator.rb +2 -0
  93. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +1 -1
  94. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_expires_at_column.erb +5 -0
  95. data/lib/generators/shopify_app/user_model/user_model_generator.rb +21 -1
  96. data/lib/shopify_app/access_scopes/noop_strategy.rb +4 -0
  97. data/lib/shopify_app/access_scopes/user_strategy.rb +9 -2
  98. data/lib/shopify_app/admin_api/with_token_refetch.rb +27 -0
  99. data/lib/shopify_app/auth/post_authenticate_tasks.rb +48 -0
  100. data/lib/shopify_app/auth/token_exchange.rb +73 -0
  101. data/lib/shopify_app/configuration.rb +82 -1
  102. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -3
  103. data/lib/shopify_app/controller_concerns/csrf_protection.rb +2 -1
  104. data/lib/shopify_app/controller_concerns/embedded_app.rb +42 -3
  105. data/lib/shopify_app/controller_concerns/ensure_billing.rb +28 -12
  106. data/lib/shopify_app/controller_concerns/frame_ancestors.rb +1 -1
  107. data/lib/shopify_app/controller_concerns/localization.rb +11 -8
  108. data/lib/shopify_app/controller_concerns/login_protection.rb +83 -38
  109. data/lib/shopify_app/controller_concerns/payload_verification.rb +1 -1
  110. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +15 -3
  111. data/lib/shopify_app/controller_concerns/sanitized_params.rb +5 -0
  112. data/lib/shopify_app/controller_concerns/token_exchange.rb +111 -0
  113. data/lib/shopify_app/controller_concerns/webhook_verification.rb +4 -1
  114. data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +48 -0
  115. data/lib/shopify_app/engine.rb +7 -8
  116. data/lib/shopify_app/logger.rb +28 -0
  117. data/lib/shopify_app/managers/webhooks_manager.rb +20 -10
  118. data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
  119. data/lib/shopify_app/session/in_memory_user_session_store.rb +1 -1
  120. data/lib/shopify_app/session/jwt.rb +11 -2
  121. data/lib/shopify_app/session/session_repository.rb +66 -14
  122. data/lib/shopify_app/session/session_storage.rb +2 -2
  123. data/lib/shopify_app/session/shop_session_storage.rb +5 -1
  124. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +5 -1
  125. data/lib/shopify_app/session/user_session_storage.rb +6 -2
  126. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +27 -2
  127. data/lib/shopify_app/test_helpers/all.rb +1 -0
  128. data/lib/shopify_app/test_helpers/shopify_session_helper.rb +16 -0
  129. data/lib/shopify_app/utils.rb +82 -20
  130. data/lib/shopify_app/version.rb +1 -1
  131. data/lib/shopify_app.rb +12 -3
  132. data/package.json +5 -6
  133. data/service.yml +0 -2
  134. data/shopify_app.gemspec +6 -5
  135. data/translation.yml +1 -0
  136. data/yarn.lock +2139 -3910
  137. metadata +78 -58
  138. data/.github/workflows/stale.yml +0 -31
  139. data/app/assets/images/storage_access.svg +0 -1
  140. data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +0 -10
  141. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +0 -22
  142. data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +0 -1
  143. data/app/assets/javascripts/shopify_app/enable_cookies.js +0 -3
  144. data/app/assets/javascripts/shopify_app/itp_helper.js +0 -40
  145. data/app/assets/javascripts/shopify_app/partition_cookies.js +0 -8
  146. data/app/assets/javascripts/shopify_app/post_redirect.js +0 -9
  147. data/app/assets/javascripts/shopify_app/request_storage_access.js +0 -3
  148. data/app/assets/javascripts/shopify_app/storage_access.js +0 -148
  149. data/app/assets/javascripts/shopify_app/storage_access_redirect.js +0 -17
  150. data/app/assets/javascripts/shopify_app/top_level.js +0 -2
  151. data/app/assets/javascripts/shopify_app/top_level_interaction.js +0 -11
  152. data/app/controllers/concerns/shopify_app/authenticated.rb +0 -19
  153. data/app/controllers/concerns/shopify_app/require_known_shop.rb +0 -48
  154. data/app/views/shopify_app/sessions/enable_cookies.html.erb +0 -70
  155. data/app/views/shopify_app/sessions/request_storage_access.html.erb +0 -68
  156. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +0 -63
  157. data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +0 -13
  158. data/docs/shopify_app/script-tags.md +0 -28
  159. data/docs/shopify_app/session-repository.md +0 -88
  160. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +0 -41
  161. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +0 -62
  162. data/lib/shopify_app/controller_concerns/itp.rb +0 -45
  163. data/lib/shopify_app/jobs/scripttags_manager_job.rb +0 -16
  164. data/lib/shopify_app/managers/scripttags_manager.rb +0 -84
@@ -0,0 +1,358 @@
1
+ # Sessions
2
+
3
+ Sessions are used to make contextual API calls for either a shop (offline session) or a user (online session). This gem has ownership of session persistence.
4
+
5
+ #### Table of contents
6
+ - [Sessions](#sessions)
7
+ - [Table of contents](#table-of-contents)
8
+ - [Sessions](#sessions-1)
9
+ - [Types of access tokens (sessions)](#types-of-access-tokens-sessions)
10
+ - [Access token storage (session)](#access-token-storage-session)
11
+ - [Shop (offline) token storage](#shop-offline-token-storage)
12
+ - [User (online) token storage](#user-online-token-storage)
13
+ - [In-memory Session Storage for testing](#in-memory-session-storage-for-testing)
14
+ - [Customizing Session Storage with `ShopifyApp::SessionRepository`](#customizing-session-storage-with-shopifyappsessionrepository)
15
+ - [⚠️ Custom Session Storage Requirements](#️--custom-session-storage-requirements)
16
+ - [Available `ActiveSupport::Concerns` that contains implementation of the above methods](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods)
17
+ - [Loading Sessions](#loading-sessions)
18
+ - [Getting Sessions with Controller Concerns](#getting-sessions-with-controller-concerns)
19
+ - [**Shop Sessions - `EnsureInstalled`**](#shop-sessions---ensureinstalled)
20
+ - [User Sessions - `EnsureHasSession`](#user-sessions---ensurehassession)
21
+ - [Getting sessions from a Shop or User model record - 'with\_shopify\_session'](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session)
22
+ - [Re-fetching an access token when API returns Unauthorized](#re-fetching-an-access-token-when-api-returns-unauthorized)
23
+ - [Access scopes](#access-scopes)
24
+ - [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes)
25
+ - [`ShopifyApp::UserSessionStorageWithScopes`](#shopifyappusersessionstoragewithscopes)
26
+ - [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy)
27
+ - [Migrating from `ShopifyApi::Auth::SessionStorage` to `ShopifyApp::SessionStorage`](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage)
28
+
29
+ ## Sessions
30
+ #### Types of access tokens (sessions)
31
+ - **Shop** ([offline access](https://shopify.dev/docs/apps/auth/access-token-types/offline))
32
+ - Access token is linked to the store
33
+ - Meant for long-term access to a store, where no user interaction is involved
34
+ - Ideal for background jobs or maintenance work
35
+ - **User** ([online access](https://shopify.dev/docs/apps/auth/access-token-types/online))
36
+ - Access token is linked to an individual user on a store
37
+ - Meant to be used when a user is interacting with your app through the web
38
+
39
+ #### Access token storage (session)
40
+ ##### Shop (offline) token storage
41
+ ⚠️ All apps must have a shop session storage, if you started from the [Ruby App Template](https://github.com/Shopify/shopify-app-template-ruby), it's already configured to have a Shop model by default.
42
+
43
+ If you don't already have a repository to store the access tokens:
44
+
45
+ 1. Run the following generator to create a shop model to store the access tokens
46
+
47
+ ```sh
48
+ rails generate shopify_app:shop_model
49
+ ```
50
+
51
+ 2. Configure `config/initializers/shopify_app.rb` to enable shop access token persistance:
52
+
53
+ ```ruby
54
+ config.shop_session_repository = 'Shop'
55
+ ```
56
+
57
+ ##### User (online) token storage
58
+ If your app has user interactions and would like to control permission based on individual users, you need to configure a User token storage to persist unique tokens for each user.
59
+
60
+ [Shop (offline) tokens must still be maintained](#shop-offline-token-storage).
61
+
62
+ 1. Run the following generator to create a user model to store the individual based access tokens.
63
+
64
+ ⚠️ If you started from the [Ruby App Template](https://github.com/Shopify/shopify-app-template-ruby), you don't need to run the generator as it's already included in the template. You can skip this step.
65
+
66
+ ```sh
67
+ rails generate shopify_app:user_model
68
+ ```
69
+
70
+ 2. Configure `config/initializers/shopify_app.rb` to enable user access token persistance:
71
+
72
+ ```ruby
73
+ config.user_session_repository = 'User'
74
+ ```
75
+
76
+ The current Shopify user will be stored in the rails session at `session[:shopify_user]`
77
+
78
+ You should also enable the [check for session expiry](#expiry-date) so that a new access token can be fetched before being used for an API operation.
79
+
80
+ ##### In-memory Session Storage for testing
81
+ The `ShopifyApp` gem includes methods for in-memory storage for both shop and user sessions. In-memory storage is intended to be used in a testing environment, please use a persistent storage for your application.
82
+ - [InMemoryShopSessionStore](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/in_memory_shop_session_store.rb)
83
+ - [InMemoryUserSessionStore](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/in_memory_user_session_store.rb)
84
+
85
+ You can configure the `ShopifyApp` configuration to use the in-memory storage method during manual testing:
86
+ ```ruby
87
+ # config/initializers/shopify_app.rb
88
+
89
+ config.shop_session_repository = ShopifyApp::InMemoryShopSessionStore
90
+ config.user_session_repository = ShopifyApp::InMemoryUserSessionStore
91
+ ```
92
+
93
+ ##### Customizing Session Storage with `ShopifyApp::SessionRepository`
94
+
95
+ In the rare event that you would like to break Rails convention for storing/retrieving records, the `ShopifyApp::SessionRepository` allows you to define how your sessions are stored and retrieved for shops. The specific repository for `shop` & `user` is configured in the `config/initializers/shopify_app.rb` file and can be set to any object.
96
+
97
+ ```ruby
98
+ # config/initializers/shopify_app.rb
99
+
100
+ config.shop_session_repository = MyCustomShopSessionRepository
101
+ config.user_session_repository = MyCustomUserSessionRepository
102
+ ```
103
+
104
+ ##### ⚠️ Custom Session Storage Requirements
105
+
106
+ The custom **Shop** repository must implement the following methods:
107
+
108
+ | Method | Parameters | Return Type |
109
+ |---------------------------------------------------|--------------------------------------------|---------------------------|
110
+ | `self.store(auth_session)` | `auth_session` (ShopifyAPI::Auth::Session) | - |
111
+ | `self.retrieve(id)` | `id` (String) | ShopifyAPI::Auth::Session |
112
+ | `self.retrieve_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | ShopifyAPI::Auth::Session |
113
+ | `self.destroy_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | - |
114
+
115
+ The custom **User** repository must implement the following methods:
116
+ | Method | Parameters | Return Type |
117
+ |---------------------------------------------|-------------------------------------|------------------------------|
118
+ | `self.store(auth_session, user)` | <li>`auth_session` (ShopifyAPI::Auth::Session)<br><li>`user` (ShopifyAPI::Auth::AssociatedUser) | - |
119
+ | `self.retrieve(id)` | `id` (String) | `ShopifyAPI::Auth::Session` |
120
+ | `self.retrieve_by_shopify_user_id(user_id)` | `user_id` (String) | `ShopifyAPI::Auth::Session` |
121
+ | `self.destroy_by_shopify_user_id(user_id)` | `user_id` (String) | - |
122
+
123
+
124
+ These methods are already implemented as a part of the `User` and `Shop` models generated from this gem's generator.
125
+ - `Shop` model includes the [ShopSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage_with_scopes.rb) concern.
126
+ - `User` model includes the [UserSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/user_session_storage_with_scopes.rb) concern.
127
+
128
+ ##### Available `ActiveSupport::Concerns` that contains implementation of the above methods
129
+ Simply include these concerns if you want to use the implementation, and overwrite methods for custom implementation
130
+
131
+ - `Shop` storage
132
+ - [ShopSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage_with_scopes.rb)
133
+ - [ShopSessionStorage](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage.rb)
134
+
135
+ - `User` storage
136
+ - [UserSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/user_session_storage_with_scopes.rb)
137
+ - [UserSessionStorage](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/user_session_storage.rb)
138
+
139
+ ### Loading Sessions
140
+ By using the appropriate controller concern, sessions are loaded for you.
141
+
142
+ #### Getting Sessions with Controller Concerns
143
+
144
+ ⚠️ **Note: These controller concerns cannot both be included in the same controller.**
145
+ ##### **Shop Sessions - `EnsureInstalled`**
146
+ - [EnsureInstalled](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_installed.rb) controller concern will load a shop session with the `installed_shop_session` helper. If a shop session is not found, meaning the app wasn't installed for this shop, the request will be redirected to be installed.
147
+ - This controller concern should NOT be used if you don't need your app to make calls on behalf of a user.
148
+ - Example
149
+ ```ruby
150
+ class MyController < ApplicationController
151
+ include ShopifyApp::EnsureInstalled
152
+
153
+ def method
154
+ current_session = installed_shop_session # `installed_shop_session` is a helper from `EnsureInstalled`
155
+
156
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_session)
157
+ client.query(
158
+ # ...
159
+ )
160
+ end
161
+ end
162
+ ```
163
+
164
+ ##### User Sessions - `EnsureHasSession`
165
+ - [EnsureHasSession](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_has_session.rb) controller concern will load a user session via `current_shopify_session`. As part of loading this session, this concern will also ensure that the user session has the appropriate scopes needed for the application and that it is not expired (when `check_session_expiry_date` is enabled). If the user isn't found or has fewer permitted scopes than are required, they will be prompted to authorize the application.
166
+ - This controller concern should be used if you don't need your app to make calls on behalf of a user. With that in mind, there are a few other embedded concerns that are mixed in to ensure that embedding, CSRF, localization, and billing allow the action for the user.
167
+ - Example
168
+ ```ruby
169
+ class MyController < ApplicationController
170
+ include ShopifyApp::EnsureHasSession
171
+
172
+ def method
173
+ current_session = current_shopify_session # `current_shopify_session` is a helper from `EnsureHasSession`
174
+
175
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_session)
176
+ client.query(
177
+ # ...
178
+ )
179
+ end
180
+ end
181
+ ```
182
+
183
+ #### Getting sessions from a Shop or User model record - 'with_shopify_session'
184
+ The [ShopifyApp::SessionStorage#with_shopify_session](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_storage.rb#L12)
185
+ helper allows you to make API calls within the context of a user or shop, by using that record's access token.
186
+
187
+ This mixin is already included in ActiveSupport [concerns](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods) from this gem.
188
+ If you're using a custom implementation of session storage, you can include the [ShopifyApp::SessionStorage](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_storage.rb) concern.
189
+
190
+ All calls made within the block passed into this helper will be made in that context:
191
+
192
+ ```ruby
193
+ # To use shop context for "my_shopify_domain.myshopify.com"
194
+ shopify_domain = "my_shopify_domain.myshopify.com"
195
+ shop = Shop.find_by(shopify_domain: shopify_domain)
196
+ shop.with_shopify_session do
197
+ ShopifyAPI::Product.find(id: product_id)
198
+ # This will call the Shopify API with my_shopify_domain's access token
199
+ end
200
+
201
+ # To use user context for user ID "my_user_id"
202
+ user = User.find_by(shopify_user_id: "my_user_id")
203
+ user.with_shopify_session do
204
+ ShopifyAPI::Product.find(id: product_id)
205
+ # This will call the Shopify API with my_user_id's access token
206
+ end
207
+ ```
208
+
209
+ #### Re-fetching an access token when API returns Unauthorized
210
+
211
+ When using `ShopifyApp::EnsureHasSession` and the `new_embedded_auth_strategy` configuration, any **unhandled** Unauthorized `ShopifyAPI::Errors::HttpResponseError` will cause the app to perform token exchange to fetch a new access token from Shopify and the action to be executed again. This will update and store the new access token to the current session instance.
212
+
213
+ ```ruby
214
+ class MyController < ApplicationController
215
+ include ShopifyApp::EnsureHasSession
216
+
217
+ def index
218
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_shopify_session)
219
+
220
+ # If this call raises an Unauthorized error from Shopify, EnsureHasSession
221
+ # will execute the action again after performing token exchange.
222
+ # It will store and use the newly retrieved access token for this and any subsequent calls.
223
+ client.query(options)
224
+ end
225
+ end
226
+ ```
227
+
228
+ If the error is being rescued in the action, it's still possible to make use of `with_token_refetch` provided by `EnsureHasSession` so that a new access token is fetched and the code is executed again with it. This will also update the session parameter with the new attributes.
229
+ This block should be used to wrap the code that makes API queries, so your business logic won't be retried.
230
+
231
+ ```ruby
232
+ class MyController < ApplicationController
233
+ include ShopifyApp::EnsureHasSession
234
+
235
+ def index
236
+ # Your app's business logic
237
+ with_token_refetch(current_shopify_session, shopify_id_token) do
238
+ # Unauthorized errors raised within this block will initiate token exchange.
239
+ # `with_token_refetch` will store the new access token and use it
240
+ # to execute this block again.
241
+ # Any subsequent calls using the same session instance will have the new token.
242
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_shopify_session)
243
+ client.query(options)
244
+ end
245
+ # Your app's business logic continues
246
+ rescue => error
247
+ # app's specific error handling
248
+ end
249
+ end
250
+ ```
251
+
252
+ It's also possible to use `with_token_refetch` on classes other than the controller by including the `ShopifyApp::AdminAPI::WithTokenRefetch` module and passing in the session along with the current request's `shopify_id_token`, which is provided by `ShopifyApp::EnsureHasSession`. This will also update the session parameter with the new attributes.
253
+
254
+ ```ruby
255
+ # my_controller.rb
256
+ class MyController < ApplicationController
257
+ include ShopifyApp::EnsureHasSession
258
+
259
+ def index
260
+ # shopify_id_token is a method provided by EnsureHasSession
261
+ MyClass.new.do_things(current_shopify_session, shopify_id_token)
262
+ end
263
+ end
264
+
265
+ # my_class.rb
266
+ class MyClass
267
+ include ShopifyApp::AdminAPI::WithTokenRefetch
268
+
269
+ def do_things(session, shopify_id_token)
270
+ with_token_refetch(session, shopify_id_token) do
271
+ # Unauthorized errors raised within this block will initiate token exchange.
272
+ # `with_token_refetch` will store the new access token and use it
273
+ # to execute this block again.
274
+ # Any subsequent calls using the same session instance will have the new token.
275
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: session)
276
+ client.query(options)
277
+ end
278
+ rescue => error
279
+ # app's specific error handling
280
+ end
281
+ end
282
+ ```
283
+
284
+ If the retried block raises an `Unauthorized` error again, `with_token_refetch` will delete the current `session` from the database and raise the error again.
285
+
286
+ ```ruby
287
+ class MyController < ApplicationController
288
+ include ShopifyApp::EnsureHasSession
289
+
290
+ def index
291
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_shopify_session)
292
+ with_token_refetch(current_shopify_session, shopify_id_token) do
293
+ # When this call raises Unauthorized a second time during retry,
294
+ # the `session` will be deleted from the database and the error raised
295
+ client.query(options)
296
+ end
297
+ rescue => error
298
+ # The Unauthorized error will reach this rescue
299
+ end
300
+ end
301
+ ```
302
+
303
+ ## Access scopes
304
+ If you want to customize how access scopes are stored for shops and users, you can implement the `access_scopes` getters and setters in the models that include `ShopifyApp::ShopSessionStorageWithScopes` and `ShopifyApp::UserSessionStorageWithScopes` as shown:
305
+
306
+ ### `ShopifyApp::ShopSessionStorageWithScopes`
307
+ ```ruby
308
+ class Shop < ActiveRecord::Base
309
+ include ShopifyApp::ShopSessionStorageWithScopes
310
+
311
+ def access_scopes=(scopes)
312
+ # Store access scopes
313
+ end
314
+ def access_scopes
315
+ # Find access scopes
316
+ end
317
+ end
318
+ ```
319
+
320
+ ### `ShopifyApp::UserSessionStorageWithScopes`
321
+ ```ruby
322
+ class User < ActiveRecord::Base
323
+ include ShopifyApp::UserSessionStorageWithScopes
324
+
325
+ def access_scopes=(scopes)
326
+ # Store access scopes
327
+ end
328
+ def access_scopes
329
+ # Find access scopes
330
+ end
331
+ end
332
+ ```
333
+
334
+ ## Expiry date
335
+ When the configuration flag `check_session_expiry_date` is set to true, the user session expiry date will be checked to trigger a re-auth and get a fresh user token when it is expired. This requires the `ShopifyAPI::Auth::Session` `expires` attribute to be stored. When the `User` model includes the `UserSessionStorageWithScopes` concern, a DB migration can be generated with `rails generate shopify_app:user_model --skip` to add the `expires_at` attribute to the model.
336
+
337
+ ## Migrating from shop-based to user-based token strategy
338
+
339
+ 1. Run the `user_model` generator as [mentioned above](#user-online-token-storage).
340
+ - The generator will ask whether you want to migrate the User model to include `access_scopes` and `expires_at` columns. `expires_at` field is useful for detecting when the user session has expired and trigger a re-auth before an operation. It can reduce
341
+ API failures for invalid access tokens. Configure the [expiry date check](#expiry-date) to complete this feature.
342
+ 2. Ensure that both your `Shop` model and `User` model includes the [necessary concerns](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods)
343
+ 3. Update the configuration file to use the new session storage.
344
+
345
+ ```ruby
346
+ # config/initializers/shopify_app.rb
347
+
348
+ config.shop_session_repository = {YOUR_SHOP_MODEL_CLASS}
349
+ config.user_session_repository = {YOUR_USER_MODEL_CLASS}
350
+ ```
351
+
352
+ ## Migrating from `ShopifyApi::Auth::SessionStorage` to `ShopifyApp::SessionStorage`
353
+ - Support for using `ShopifyApi::Auth::SessionStorage` was removed from ShopifyApi [version 13.0.0](https://github.com/Shopify/shopify-api-ruby/blob/main/CHANGELOG.md#1300)
354
+ - Sessions storage are now handled with [ShopifyApp::SessionRepository](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_repository.rb)
355
+ - To migrate and specify your shop or user session storage method:
356
+ 1. Remove `session_storage` configuration from `config/initializers/shopify_app.rb`
357
+ 2. Follow ["Access Token Storage" instructions](#access-token-storage-session) to specify the storage repository for shop and user sessions.
358
+ - [Customizing session storage](#customizing-session-storage-with-shopifyappsessionrepository)
@@ -11,20 +11,42 @@
11
11
  A test helper that will allow you to test `ShopifyApp::WebhookVerification` in the controller from your app, to use this test, you need to `require` it directly inside your app `test/controllers/webhook_verification_test.rb`.
12
12
 
13
13
  ```ruby
14
- require 'test_helper'
15
- require 'action_controller'
16
- require 'action_controller/base'
17
- require 'shopify_app/test_helpers/webhook_verification_helper'
14
+ require 'test_helper'
15
+ require 'action_controller'
16
+ require 'action_controller/base'
17
+ require 'shopify_app/test_helpers/webhook_verification_helper'
18
18
  ```
19
19
 
20
- Or you can require in your `test/test_helper.rb`.
20
+ A test helper that allows you to stub out a shopify_app session in controllers that include `ShopifyApp::LoginProtection`, to use this helper, you need to `require` it directly.
21
+
22
+ Example Usage:
23
+
24
+ ```ruby
25
+ require 'shopify_app/test_helpers/shopify_session_helper'
26
+
27
+ class MyAuthenticatedControllerTest < ActionController::TestCase
28
+ include ShopifyApp::TestHelpers::ShopifySessionHelper
29
+
30
+ test "does not redirect when there is a valid shopify session" do
31
+ # note shop_domain should be the same as your shopify domain
32
+ shop_domain = "my-shop.myshopify.com"
33
+ setup_shopify_session(session_id: "1", shop_domain: shop_domain)
34
+
35
+ get :index
36
+
37
+ assert_response :ok
38
+ end
39
+ end
40
+ ```
41
+
42
+ Or you can require all shopify_app test helpers in your `test/test_helper.rb`.
21
43
 
22
44
  ```ruby
23
- ENV['RAILS_ENV'] ||= 'test'
24
- require_relative '../config/environment'
25
- require 'rails/test_help'
26
- require 'byebug'
27
- require 'shopify_app/test_helpers/all'
45
+ ENV['RAILS_ENV'] ||= 'test'
46
+ require_relative '../config/environment'
47
+ require 'rails/test_help'
48
+ require 'byebug'
49
+ require 'shopify_app/test_helpers/all'
28
50
  ```
29
51
 
30
52
  With `lib/shopify_app/test_helpers/all'` more tests can be added and will only need to be required in once in your library.
@@ -2,22 +2,38 @@
2
2
 
3
3
  #### Table of contents
4
4
 
5
- [Manage webhooks using `ShopifyApp::WebhooksManager`](#manage-webhooks-using-shopifyappwebhooksmanager)
5
+ [App-specific webhooks in shopify.app.toml (recommended)](#subscribing-to-webhooks-in-the-app-configuration-file)
6
+ [Manage shop-specific webhooks using `ShopifyApp::WebhooksManager`](#manage-webhooks-using-shopifyappwebhooksmanager)
7
+ [Mandatory Privacy Webhooks](#mandatory-privacy-webhooks)
6
8
 
7
- ## Manage webhooks using `ShopifyApp::WebhooksManager`
9
+ ## App-specific webhooks in shopify.app.toml (recommended)
10
+ You can specify app-specific webhooks to subscribe to in the `shopify.app.toml` file. These subscriptions are easier to manage because they are kept up to date by Shopify. In many cases they will be sufficient. Please read [app-specific vs shop-specific subscriptions](https://shopify.dev/docs/apps/build/webhooks/subscribe#app-specific-vs-shop-specific-subscriptions) to understand when you might need shop-specific webhooks.
8
11
 
9
- See [`ShopifyApp::WebhooksManager`](/lib/shopify_app/managers/webhooks_manager.rb)
10
- ShopifyApp can manage your app's webhooks for you if you set which webhooks you require in the initializer:
12
+ ### Consuming app-specific webhooks events
13
+ To consume app-specific webhooks events from the `shopify.app.toml` file, you can scaffold the necessary files by running the following generator.
14
+
15
+ ```bash
16
+ rails g shopify_app:add_declarative_webhook --topic carts/update --path webhooks/carts_update
17
+ ```
18
+
19
+ This will add a new controller, job, and route to your application. The controller will verify the webhook and queue the job to process the webhook. The job will be responsible for processing the webhook data.
20
+
21
+ ## Manage shop-specific webhooks using `ShopifyApp::WebhooksManager`
22
+
23
+ See [`ShopifyApp::WebhooksManager`](/lib/shopify_app/managers/webhooks_manager.rb).
24
+ ShopifyApp can manage your app's shop-specific webhooks for you if you set which webhooks you require in the initializer:
11
25
 
12
26
  ```ruby
13
27
  ShopifyApp.configure do |config|
14
28
  config.webhooks = [
15
- {topic: 'carts/update', path: 'webhooks/carts_update'}
29
+ {topic: 'carts/update', path: 'api/webhooks/carts_update'}
16
30
  ]
17
31
  end
18
32
  ```
19
33
 
20
- When the [OAuth callback](/docs/shopify_app/authentication.md#oauth-callback) is completed successfully, ShopifyApp will queue a background job which will ensure all the specified webhooks exist for that shop. Because this runs on every OAuth callback, it means your app will always have the webhooks it needs even if the user uninstalls and re-installs the app.
34
+ This method should only be used if you have a good reason not to use app-specific webhooks (such as requiring different topics for different shops).
35
+
36
+ When the [OAuth callback](/docs/shopify_app/authentication.md#oauth-callback) or token exchange is completed successfully, ShopifyApp will queue a background job which will ensure all the specified webhooks exist for that shop. Because this runs on every OAuth callback, it means your app will always have the webhooks it needs even if the user uninstalls and re-installs the app.
21
37
 
22
38
  ShopifyApp also provides a [WebhooksController](/app/controllers/shopify_app/webhooks_controller.rb) that receives webhooks and queues a job based on the received topic. For example, if you register the webhook from above, then all you need to do is create a job called `CartsUpdateJob`. The job will be queued with 2 params: `shop_domain` and `webhook` (which is the webhook body).
23
39
 
@@ -34,7 +50,35 @@ If you are only interested in particular fields, you can optionally filter the d
34
50
  ```ruby
35
51
  ShopifyApp.configure do |config|
36
52
  config.webhooks = [
37
- {topic: 'products/update', path: 'webhooks/products_update', fields: ['title', 'vendor']}
53
+ {topic: 'products/update', path: 'api/webhooks/products_update', fields: ['title', 'vendor']}
54
+ ]
55
+ end
56
+ ```
57
+
58
+ If you need to read metafields, you can pass in the `metafield_namespaces` parameter in `config/webhooks`. Note if you are also using the `fields` parameter you will need to add `metafields` into that as well. Shopify documentation on metafields in webhooks can be found [here](https://shopify.dev/docs/api/admin-rest/2023-10/resources/webhook#resource-object).
59
+
60
+ ```ruby
61
+ ShopifyApp.configure do |config|
62
+ config.webhooks = [
63
+ {
64
+ topic: 'orders/create',
65
+ path: 'api/webhooks/orders_create',
66
+ metafield_namespaces: ['app-namespace'],
67
+ },
68
+ ]
69
+ end
70
+ ```
71
+
72
+ If you need to filter by webhook fields, you can register a webhook with a `filter` parameter. The documentation for Webhook filters can be found [here](https://shopify.dev/docs/apps/build/webhooks/customize/filters).
73
+
74
+ ```ruby
75
+ ShopifyApp.configure do |config|
76
+ config.webhooks = [
77
+ {
78
+ topic: 'products/update',
79
+ path: 'api/webhooks/products_update',
80
+ filter: "variants.price:>=10.00",
81
+ },
38
82
  ]
39
83
  end
40
84
  ```
@@ -70,3 +114,49 @@ rails g shopify_app:add_webhook --topic carts/update --path webhooks/carts_updat
70
114
  ```
71
115
 
72
116
  Where `--topic` is the topic and `--path` is the path the webhook should be sent to.
117
+
118
+ ## Mandatory Privacy Webhooks
119
+
120
+ We have three mandatory privacy webhooks
121
+
122
+ 1. `customers/data_request`
123
+ 2. `customer/redact`
124
+ 3. `shop/redact`
125
+
126
+ The `generate shopify_app` command generated three job templates corresponding to all three of these webhooks.
127
+ To pass our approval process you will need to set these webhooks in your partner dashboard.
128
+ You can read more about that [here](https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks).
129
+
130
+ ## EventBridge and PubSub Webhooks
131
+
132
+ You can also register webhooks for delivery to Amazon EventBridge or Google Cloud Pub/Sub. In this case the `path` argument to needs to be of a specific form.
133
+
134
+ For EventBridge, the `path` must be the ARN of the partner event source.
135
+
136
+ ```rb
137
+ ShopifyApp.configure do |config|
138
+ config.webhooks = [
139
+ {
140
+ delivery_method: :event_bridge,
141
+ topic: 'carts/update',
142
+ path: 'arn:aws:events....'
143
+ }
144
+ ]
145
+ end
146
+ ```
147
+
148
+ For Pub/Sub, the `path` must be of the form `pubsub://[PROJECT-ID]:[PUB-SUB-TOPIC-ID]`. For example, if you created a topic with id `red` in the project `blue`, then the value of path would be `pubsub://blue:red`.
149
+
150
+ ```rb
151
+ ShopifyApp.configure do |config|
152
+ config.webhooks = [
153
+ {
154
+ delivery_method: :pub_sub,
155
+ topic: 'carts/update',
156
+ path: 'pubsub://project-id:pub-sub-topic-id'
157
+ }
158
+ ]
159
+ end
160
+ ```
161
+
162
+ When registering for an EventBridge or PubSub Webhook you'll need to implement a handler that will fetch webhooks from the queue and process them yourself.
data/karma.conf.js CHANGED
@@ -16,9 +16,6 @@ module.exports = function(config) {
16
16
  exclude: [
17
17
  // Exclude JS files that create 'DOMContentLoaded' event listeners
18
18
  'app/assets/javascripts/**/redirect.js',
19
- 'app/assets/javascripts/**/storage_access_redirect.js',
20
- 'app/assets/javascripts/**/top_level_interaction.js',
21
- 'app/assets/javascripts/**/partition_cookies.js',
22
19
  ],
23
20
  mochaReporter: {
24
21
  output: 'autowatch',
@@ -26,7 +23,12 @@ module.exports = function(config) {
26
23
  preprocessors: {
27
24
  'test/javascripts/**/*test.js': ['webpack'],
28
25
  },
29
- webpack: {},
26
+ webpack: {
27
+ mode: 'none',
28
+ output: {
29
+ hashFunction: 'rsa-sha512',
30
+ },
31
+ },
30
32
  reporters: karmaReporters,
31
33
  port: 9876,
32
34
  colors: true,
@@ -21,12 +21,15 @@ module ShopifyApp
21
21
  inject_into_file(
22
22
  "config/initializers/shopify_app.rb",
23
23
  after_authenticate_job_config,
24
- before: "end"
24
+ before: "end",
25
25
  )
26
26
 
27
27
  unless initializer.include?(after_authenticate_job_config)
28
- shell.say("Error adding after_authenticate_job to config. Add this line manually: "\
29
- "#{after_authenticate_job_config}", :red)
28
+ shell.say(
29
+ "Error adding after_authenticate_job to config. Add this line manually: "\
30
+ "#{after_authenticate_job_config}",
31
+ :red,
32
+ )
30
33
  end
31
34
  end
32
35
 
@@ -5,7 +5,7 @@ module Shopify
5
5
  def perform(shop_domain:)
6
6
  shop = Shop.find_by(shopify_domain: shop_domain)
7
7
 
8
- shop.with_shopify_session do
8
+ shop.with_shopify_session do |session|
9
9
  end
10
10
  end
11
11
  end
@@ -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