shopify_app 17.0.3 → 17.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -0
- data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
- data/.github/ISSUE_TEMPLATE/config.yml +1 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +17 -1
- data/.github/workflows/build.yml +4 -1
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +76 -0
- data/Gemfile.lock +81 -80
- data/README.md +72 -593
- data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +4 -0
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
- data/app/controllers/shopify_app/callback_controller.rb +24 -2
- data/app/controllers/shopify_app/sessions_controller.rb +13 -4
- data/config/locales/nl.yml +1 -1
- data/docs/Quickstart.md +15 -77
- data/docs/Troubleshooting.md +129 -4
- data/docs/Upgrading.md +126 -0
- data/docs/shopify_app/authentication.md +124 -0
- data/docs/shopify_app/engine.md +82 -0
- data/docs/shopify_app/generators.md +127 -0
- data/docs/shopify_app/handling-access-scopes-changes.md +14 -0
- data/docs/shopify_app/script-tags.md +28 -0
- data/docs/shopify_app/session-repository.md +88 -0
- data/docs/shopify_app/testing.md +38 -0
- data/docs/shopify_app/webhooks.md +72 -0
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +2 -0
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -0
- data/lib/generators/shopify_app/install/install_generator.rb +30 -1
- data/lib/generators/shopify_app/install/templates/omniauth.rb +1 -0
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +5 -2
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +27 -0
- data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
- data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -1
- data/lib/generators/shopify_app/shopify_app_generator.rb +1 -1
- data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
- data/lib/generators/shopify_app/user_model/templates/user.rb +1 -1
- data/lib/generators/shopify_app/user_model/user_model_generator.rb +27 -0
- data/lib/shopify_app.rb +10 -0
- data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
- data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
- data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
- data/lib/shopify_app/configuration.rb +22 -0
- data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +1 -1
- data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
- data/lib/shopify_app/session/in_memory_shop_session_store.rb +9 -7
- data/lib/shopify_app/session/in_memory_user_session_store.rb +9 -7
- data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
- data/lib/shopify_app/utils.rb +12 -0
- data/lib/shopify_app/version.rb +1 -1
- data/package.json +1 -1
- data/shopify_app.gemspec +2 -2
- metadata +29 -10
- data/.github/ISSUE_TEMPLATE.md +0 -19
- data/docs/install-on-dev-shop.png +0 -0
- data/docs/test-your-app.png +0 -0
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -20
@@ -0,0 +1,38 @@
|
|
1
|
+
# Testing
|
2
|
+
|
3
|
+
#### Table of contents
|
4
|
+
|
5
|
+
[Using test helpers inside your application](#using-test-helpers-inside-your-application)
|
6
|
+
|
7
|
+
[Testing an embedded app outside the Shopify admin](#testing-an-embedded-app-outside-the-shopify-admin)
|
8
|
+
|
9
|
+
## Using test helpers inside your application
|
10
|
+
|
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
|
+
|
13
|
+
```ruby
|
14
|
+
require 'test_helper'
|
15
|
+
require 'action_controller'
|
16
|
+
require 'action_controller/base'
|
17
|
+
require 'shopify_app/test_helpers/webhook_verification_helper'
|
18
|
+
```
|
19
|
+
|
20
|
+
Or you can require in your `test/test_helper.rb`.
|
21
|
+
|
22
|
+
```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'
|
28
|
+
```
|
29
|
+
|
30
|
+
With `lib/shopify_app/test_helpers/all'` more tests can be added and will only need to be required in once in your library.
|
31
|
+
|
32
|
+
## Testing an embedded app outside the Shopify admin
|
33
|
+
|
34
|
+
By default, loading your embedded app will redirect to the Shopify admin, with the app view loaded in an `iframe`. If you need to load your app outside of the Shopify admin (e.g., for performance testing), you can change `forceRedirect: true` to `false` in `ShopifyApp.init` block in the `embedded_app` view. To keep the redirect on in production but off in your `development` and `test` environments, you can use:
|
35
|
+
|
36
|
+
```javascript
|
37
|
+
forceRedirect: <%= Rails.env.development? || Rails.env.test? ? 'false' : 'true' %>
|
38
|
+
```
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Webhooks
|
2
|
+
|
3
|
+
#### Table of contents
|
4
|
+
|
5
|
+
[Manage webhooks using `ShopifyApp::WebhooksManager`](#manage-webhooks-using-shopifyappwebhooksmanager)
|
6
|
+
|
7
|
+
## Manage webhooks using `ShopifyApp::WebhooksManager`
|
8
|
+
|
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:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
ShopifyApp.configure do |config|
|
14
|
+
config.webhooks = [
|
15
|
+
{topic: 'carts/update', address: 'https://example-app.com/webhooks/carts_update'}
|
16
|
+
]
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
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.
|
21
|
+
|
22
|
+
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
|
+
|
24
|
+
If you would like to namespace your jobs, you may set `webhook_jobs_namespace` in the config. For example, if your app handles webhooks from other ecommerce applications as well, and you want Shopify cart update webhooks to be processed by a job living in `jobs/shopify/webhooks/carts_update_job.rb` rather than `jobs/carts_update_job.rb`):
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
ShopifyApp.configure do |config|
|
28
|
+
config.webhook_jobs_namespace = 'shopify/webhooks'
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
If you are only interested in particular fields, you can optionally filter the data sent by Shopify by specifying the `fields` parameter in `config/webhooks`. Note that you will still receive a webhook request from Shopify every time the resource is updated, but only the specified fields will be sent.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
ShopifyApp.configure do |config|
|
36
|
+
config.webhooks = [
|
37
|
+
{topic: 'products/update', address: 'https://example-app.com/webhooks/products_update', fields: ['title', 'vendor']}
|
38
|
+
]
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
If you'd rather implement your own controller then you'll want to use the [`ShopifyApp::WebhookVerification`](/lib/shopify_app/controller_concerns/webhook_verification.rb) module to verify your webhooks, example:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class CustomWebhooksController < ApplicationController
|
46
|
+
include ShopifyApp::WebhookVerification
|
47
|
+
|
48
|
+
def carts_update
|
49
|
+
params.permit!
|
50
|
+
SomeJob.perform_later(shop_domain: shop_domain, webhook: webhook_params.to_h)
|
51
|
+
head :no_content
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def webhook_params
|
57
|
+
params.except(:controller, :action, :type)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
The module skips the `verify_authenticity_token` before_action and adds an action to verify that the webhook came from Shopify. You can now add a post route to your application, pointing to the controller and action to accept the webhook data from Shopify.
|
63
|
+
|
64
|
+
The WebhooksManager uses ActiveJob. If ActiveJob is not configured then by default Rails will run the jobs inline. However, it is highly recommended to configure a proper background processing queue like Sidekiq or Resque in production.
|
65
|
+
|
66
|
+
ShopifyApp can create webhooks for you using the `add_webhook` generator. This will add the new webhook to your config and create the required job class for you.
|
67
|
+
|
68
|
+
```
|
69
|
+
rails g shopify_app:add_webhook -t carts/update -a https://example.com/webhooks/carts_update
|
70
|
+
```
|
71
|
+
|
72
|
+
Where `-t` is the topic and `-a` is the address the webhook should be sent to.
|
@@ -30,9 +30,11 @@ module ShopifyApp
|
|
30
30
|
copy_file('omniauth.rb', 'config/initializers/omniauth.rb')
|
31
31
|
end
|
32
32
|
|
33
|
+
return if !Rails.env.test? && shopify_provider_exists?
|
34
|
+
|
33
35
|
inject_into_file(
|
34
36
|
'config/initializers/omniauth.rb',
|
35
|
-
|
37
|
+
shopify_provider_template,
|
36
38
|
after: "Rails.application.config.middleware.use(OmniAuth::Builder) do\n"
|
37
39
|
)
|
38
40
|
end
|
@@ -72,6 +74,33 @@ module ShopifyApp
|
|
72
74
|
|
73
75
|
private
|
74
76
|
|
77
|
+
def shopify_provider_exists?
|
78
|
+
File.open("config/initializers/omniauth.rb") do |file|
|
79
|
+
file.each_line do |line|
|
80
|
+
if line =~ /provider :shopify/
|
81
|
+
puts "\e[33m#{omniauth_warning}\e[0m"
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
def omniauth_warning
|
90
|
+
<<~OMNIAUTH
|
91
|
+
\n[WARNING] The Shopify App generator attempted to add the following Shopify Omniauth \
|
92
|
+
provider 'config/initializers/omniauth.rb':
|
93
|
+
|
94
|
+
\e[0m#{shopify_provider_template}\e[33m
|
95
|
+
|
96
|
+
Consider updating 'config/initializers/omniauth.rb' to match the configuration above.
|
97
|
+
OMNIAUTH
|
98
|
+
end
|
99
|
+
|
100
|
+
def shopify_provider_template
|
101
|
+
File.read(File.expand_path(find_in_source_paths('shopify_provider.rb.tt')))
|
102
|
+
end
|
103
|
+
|
75
104
|
def embedded_app?
|
76
105
|
options['embedded'] == 'true'
|
77
106
|
end
|
@@ -7,14 +7,17 @@ ShopifyApp.configure do |config|
|
|
7
7
|
config.after_authenticate_job = false
|
8
8
|
config.api_version = "<%= @api_version %>"
|
9
9
|
config.shop_session_repository = 'Shop'
|
10
|
+
|
11
|
+
config.reauth_on_access_scope_changes = true
|
12
|
+
|
10
13
|
config.allow_jwt_authentication = <%= !with_cookie_authentication? %>
|
11
14
|
config.allow_cookie_authentication = <%= with_cookie_authentication? %>
|
12
15
|
|
13
16
|
config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence
|
14
17
|
config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence
|
15
18
|
if defined? Rails::Server
|
16
|
-
raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#
|
17
|
-
raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#
|
19
|
+
raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#requirements') unless config.api_key
|
20
|
+
raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#requirements') unless config.secret
|
18
21
|
end
|
19
22
|
end
|
20
23
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
provider :shopify,
|
2
|
+
ShopifyApp.configuration.api_key,
|
3
|
+
ShopifyApp.configuration.secret,
|
4
|
+
scope: ShopifyApp.configuration.scope,
|
5
|
+
setup: lambda { |env|
|
6
|
+
configuration = ShopifyApp::OmniAuthConfiguration.new(env['omniauth.strategy'], Rack::Request.new(env))
|
7
|
+
configuration.build_options
|
8
|
+
}
|
@@ -8,6 +8,8 @@ module ShopifyApp
|
|
8
8
|
include Rails::Generators::Migration
|
9
9
|
source_root File.expand_path('../templates', __FILE__)
|
10
10
|
|
11
|
+
class_option :new_shopify_cli_app, type: :boolean, default: false
|
12
|
+
|
11
13
|
def create_shop_model
|
12
14
|
copy_file('shop.rb', 'app/models/shop.rb')
|
13
15
|
end
|
@@ -16,6 +18,27 @@ module ShopifyApp
|
|
16
18
|
migration_template('db/migrate/create_shops.erb', 'db/migrate/create_shops.rb')
|
17
19
|
end
|
18
20
|
|
21
|
+
def create_shop_with_access_scopes_migration
|
22
|
+
scopes_column_prompt = <<~PROMPT
|
23
|
+
It is highly recommended that apps record the access scopes granted by \
|
24
|
+
merchants during app installation. See app/models/shop.rb to modify how \
|
25
|
+
access scopes are stored and retrieved.
|
26
|
+
|
27
|
+
[WARNING] You will need to update the access_scopes accessors in the Shop model \
|
28
|
+
to allow shopify_app to store and retrieve scopes when going through OAuth.
|
29
|
+
|
30
|
+
The following migration will add an `access_scopes` column to the Shop model. \
|
31
|
+
Do you want to include this migration? [y/n]
|
32
|
+
PROMPT
|
33
|
+
|
34
|
+
if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
|
35
|
+
migration_template(
|
36
|
+
'db/migrate/add_shop_access_scopes_column.erb',
|
37
|
+
'db/migrate/add_shop_access_scopes_column.rb'
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
19
42
|
def update_shopify_app_initializer
|
20
43
|
gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryShopSessionStore', 'Shop')
|
21
44
|
end
|
@@ -26,6 +49,10 @@ module ShopifyApp
|
|
26
49
|
|
27
50
|
private
|
28
51
|
|
52
|
+
def new_shopify_cli_app?
|
53
|
+
options['new_shopify_cli_app']
|
54
|
+
end
|
55
|
+
|
29
56
|
def rails_migration_version
|
30
57
|
Rails.version.match(/\d\.\d/)[0]
|
31
58
|
end
|
@@ -9,7 +9,7 @@ module ShopifyApp
|
|
9
9
|
|
10
10
|
def run_all_generators
|
11
11
|
generate("shopify_app:install #{@opts.join(' ')}")
|
12
|
-
generate("shopify_app:shop_model")
|
12
|
+
generate("shopify_app:shop_model #{@opts.join(' ')}")
|
13
13
|
generate("shopify_app:authenticated_controller")
|
14
14
|
generate("shopify_app:home_controller #{@opts.join(' ')}")
|
15
15
|
end
|
@@ -8,6 +8,8 @@ module ShopifyApp
|
|
8
8
|
include Rails::Generators::Migration
|
9
9
|
source_root File.expand_path('../templates', __FILE__)
|
10
10
|
|
11
|
+
class_option :new_shopify_cli_app, type: :boolean, default: false
|
12
|
+
|
11
13
|
def create_user_model
|
12
14
|
copy_file('user.rb', 'app/models/user.rb')
|
13
15
|
end
|
@@ -16,6 +18,27 @@ module ShopifyApp
|
|
16
18
|
migration_template('db/migrate/create_users.erb', 'db/migrate/create_users.rb')
|
17
19
|
end
|
18
20
|
|
21
|
+
def create_scopes_storage_in_user_model
|
22
|
+
scopes_column_prompt = <<~PROMPT
|
23
|
+
It is highly recommended that apps record the access scopes granted by \
|
24
|
+
merchants during app installation. See app/models/user.rb to modify how \
|
25
|
+
access scopes are stored and retrieved.
|
26
|
+
|
27
|
+
[WARNING] You will need to update the access_scopes accessors in the User model \
|
28
|
+
to allow shopify_app to store and retrieve scopes when going through OAuth.
|
29
|
+
|
30
|
+
The following migration will add an `access_scopes` column to the User model. \
|
31
|
+
Do you want to include this migration? [y/n]
|
32
|
+
PROMPT
|
33
|
+
|
34
|
+
if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
|
35
|
+
migration_template(
|
36
|
+
'db/migrate/add_user_access_scopes_column.erb',
|
37
|
+
'db/migrate/add_user_access_scopes_column.rb'
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
19
42
|
def update_shopify_app_initializer
|
20
43
|
gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryUserSessionStore', 'User')
|
21
44
|
end
|
@@ -26,6 +49,10 @@ module ShopifyApp
|
|
26
49
|
|
27
50
|
private
|
28
51
|
|
52
|
+
def new_shopify_cli_app?
|
53
|
+
options['new_shopify_cli_app']
|
54
|
+
end
|
55
|
+
|
29
56
|
def rails_migration_version
|
30
57
|
Rails.version.match(/\d\.\d/)[0]
|
31
58
|
end
|
data/lib/shopify_app.rb
CHANGED
@@ -57,5 +57,15 @@ module ShopifyApp
|
|
57
57
|
require 'shopify_app/session/session_repository'
|
58
58
|
require 'shopify_app/session/session_storage'
|
59
59
|
require 'shopify_app/session/shop_session_storage'
|
60
|
+
require 'shopify_app/session/shop_session_storage_with_scopes'
|
60
61
|
require 'shopify_app/session/user_session_storage'
|
62
|
+
require 'shopify_app/session/user_session_storage_with_scopes'
|
63
|
+
|
64
|
+
# access scopes strategies
|
65
|
+
require 'shopify_app/access_scopes/shop_strategy'
|
66
|
+
require 'shopify_app/access_scopes/user_strategy'
|
67
|
+
require 'shopify_app/access_scopes/noop_strategy'
|
68
|
+
|
69
|
+
# omniauth_configuration
|
70
|
+
require 'shopify_app/omniauth/omniauth_configuration'
|
61
71
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module AccessScopes
|
5
|
+
class ShopStrategy
|
6
|
+
class << self
|
7
|
+
def update_access_scopes?(shop_domain)
|
8
|
+
shop_access_scopes = shop_access_scopes(shop_domain)
|
9
|
+
configuration_access_scopes != shop_access_scopes
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def shop_access_scopes(shop_domain)
|
15
|
+
ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop_domain)&.access_scopes
|
16
|
+
end
|
17
|
+
|
18
|
+
def configuration_access_scopes
|
19
|
+
ShopifyAPI::ApiAccess.new(ShopifyApp.configuration.shop_access_scopes)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module AccessScopes
|
5
|
+
class UserStrategy
|
6
|
+
class InvalidInput < StandardError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def update_access_scopes?(user_id: nil, shopify_user_id: nil)
|
10
|
+
return update_access_scopes_for_user_id?(user_id) if user_id
|
11
|
+
return update_access_scopes_for_shopify_user_id?(shopify_user_id) if shopify_user_id
|
12
|
+
raise(InvalidInput, '#update_access_scopes? requires user_id or shopify_user_id parameter inputs')
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def update_access_scopes_for_user_id?(user_id)
|
18
|
+
user_access_scopes = user_access_scopes_by_user_id(user_id)
|
19
|
+
configuration_access_scopes != user_access_scopes
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_access_scopes_for_shopify_user_id?(shopify_user_id)
|
23
|
+
user_access_scopes = user_access_scopes_by_shopify_user_id(shopify_user_id)
|
24
|
+
configuration_access_scopes != user_access_scopes
|
25
|
+
end
|
26
|
+
|
27
|
+
def user_access_scopes_by_user_id(user_id)
|
28
|
+
ShopifyApp::SessionRepository.retrieve_user_session(user_id)&.access_scopes
|
29
|
+
end
|
30
|
+
|
31
|
+
def user_access_scopes_by_shopify_user_id(shopify_user_id)
|
32
|
+
ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(shopify_user_id)&.access_scopes
|
33
|
+
end
|
34
|
+
|
35
|
+
def configuration_access_scopes
|
36
|
+
ShopifyAPI::ApiAccess.new(ShopifyApp.configuration.user_access_scopes)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|