shopify_app 14.4.4 → 18.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
  4. data/.github/ISSUE_TEMPLATE/config.yml +1 -0
  5. data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
  6. data/.github/PULL_REQUEST_TEMPLATE.md +17 -1
  7. data/.github/workflows/build.yml +41 -0
  8. data/.github/workflows/release.yml +24 -0
  9. data/.github/workflows/rubocop.yml +1 -7
  10. data/.gitignore +0 -2
  11. data/CHANGELOG.md +75 -0
  12. data/CONTRIBUTING.md +76 -0
  13. data/Gemfile.lock +268 -0
  14. data/README.md +73 -572
  15. data/app/assets/images/storage_access.svg +1 -2
  16. data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
  17. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +26 -0
  18. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
  19. data/app/controllers/shopify_app/callback_controller.rb +34 -10
  20. data/app/controllers/shopify_app/sessions_controller.rb +19 -20
  21. data/app/views/shopify_app/partials/_button_styles.html.erb +41 -36
  22. data/app/views/shopify_app/partials/_card_styles.html.erb +3 -3
  23. data/app/views/shopify_app/partials/_empty_state_styles.html.erb +28 -59
  24. data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
  25. data/app/views/shopify_app/partials/_layout_styles.html.erb +16 -1
  26. data/app/views/shopify_app/partials/_typography_styles.html.erb +6 -6
  27. data/app/views/shopify_app/sessions/enable_cookies.html.erb +1 -1
  28. data/app/views/shopify_app/sessions/new.html.erb +38 -110
  29. data/app/views/shopify_app/sessions/request_storage_access.html.erb +1 -1
  30. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +20 -15
  31. data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
  32. data/config/locales/de.yml +11 -11
  33. data/config/locales/nl.yml +1 -1
  34. data/config/locales/vi.yml +22 -0
  35. data/config/locales/zh-CN.yml +1 -1
  36. data/docs/Quickstart.md +15 -77
  37. data/docs/Releasing.md +9 -6
  38. data/docs/Troubleshooting.md +147 -4
  39. data/docs/Upgrading.md +126 -0
  40. data/docs/shopify_app/authentication.md +124 -0
  41. data/docs/shopify_app/engine.md +82 -0
  42. data/docs/shopify_app/generators.md +127 -0
  43. data/docs/shopify_app/handling-access-scopes-changes.md +14 -0
  44. data/docs/shopify_app/script-tags.md +28 -0
  45. data/docs/shopify_app/session-repository.md +88 -0
  46. data/docs/shopify_app/testing.md +38 -0
  47. data/docs/shopify_app/webhooks.md +72 -0
  48. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +16 -7
  49. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +10 -0
  50. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +11 -11
  51. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +2 -0
  52. data/lib/generators/shopify_app/install/install_generator.rb +36 -2
  53. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +2 -1
  54. data/lib/generators/shopify_app/install/templates/omniauth.rb +1 -0
  55. data/lib/generators/shopify_app/install/templates/shopify_app.js +1 -1
  56. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +13 -4
  57. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
  58. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +27 -0
  59. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
  60. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -1
  61. data/lib/generators/shopify_app/shopify_app_generator.rb +1 -1
  62. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
  63. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -1
  64. data/lib/generators/shopify_app/user_model/user_model_generator.rb +27 -0
  65. data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
  66. data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
  67. data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
  68. data/lib/shopify_app/configuration.rb +25 -0
  69. data/lib/shopify_app/controller_concerns/itp.rb +0 -2
  70. data/lib/shopify_app/controller_concerns/login_protection.rb +14 -17
  71. data/lib/shopify_app/engine.rb +1 -0
  72. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +1 -1
  73. data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
  74. data/lib/shopify_app/session/in_memory_shop_session_store.rb +9 -7
  75. data/lib/shopify_app/session/in_memory_user_session_store.rb +9 -7
  76. data/lib/shopify_app/session/jwt.rb +3 -1
  77. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
  78. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
  79. data/lib/shopify_app/utils.rb +12 -0
  80. data/lib/shopify_app/version.rb +1 -1
  81. data/lib/shopify_app.rb +11 -0
  82. data/package.json +1 -1
  83. data/service.yml +1 -4
  84. data/shopify_app.gemspec +5 -4
  85. data/translation.yml +1 -1
  86. data/yarn.lock +92 -123
  87. metadata +62 -16
  88. data/.github/ISSUE_TEMPLATE.md +0 -19
  89. data/.travis.yml +0 -27
  90. data/docs/install-on-dev-shop.png +0 -0
  91. data/docs/test-your-app.png +0 -0
  92. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -20
@@ -0,0 +1,88 @@
1
+ # Session repository
2
+
3
+ #### Table of contents
4
+
5
+ [`ShopifyApp::SessionRepository`](#shopifyappsessionrepository)
6
+ * [Shop-based token storage](#shop-based-token-storage)
7
+ * [User-based token storage](#user-based-token-storage)
8
+
9
+ [Access scopes](#access-scopes)
10
+ * [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes)
11
+ * [``ShopifyApp::UserSessionStorageWithScopes``](#shopifyappusersessionstoragewithscopes)
12
+
13
+ [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy)
14
+
15
+ ## ShopifyApp::SessionRepository
16
+
17
+ `ShopifyApp::SessionRepository` allows you as a developer to define how your sessions are stored and retrieved for shops. The `SessionRepository` is configured in the `config/initializers/shopify_app.rb` file and can be set to any object that implements `self.store(auth_session, *args)` which stores the session and returns a unique identifier and `self.retrieve(id)` which returns a `ShopifyAPI::Session` for the passed id. These methods are already implemented as part of the `ShopifyApp::SessionStorage` concern but can be overridden for custom implementation.
18
+
19
+ ### Shop-based token storage
20
+
21
+ Storing tokens on the store model means that any user login associated with the store will have equal access levels to whatever the original user granted the app.
22
+ ```sh
23
+ $ rails generate shopify_app:shop_model
24
+ ```
25
+ This will generate a shop model which will be the storage for the tokens necessary for authentication.
26
+
27
+ ### User-based token storage
28
+
29
+ A more granular control over the level of access per user on an app might be necessary, to which the shop-based token strategy is not sufficient. Shopify supports a user-based token storage strategy where a unique token to each user can be managed. Shop tokens must still be maintained if you are running background jobs so that you can make use of them when necessary.
30
+ ```sh
31
+ $ rails generate shopify_app:shop_model
32
+ $ rails generate shopify_app:user_model
33
+ ```
34
+ This will generate a shop model and user model, which will be the storage for the tokens necessary for authentication.
35
+
36
+ The current Shopify user will be stored in the rails session at `session[:shopify_user]`
37
+
38
+ Read more about Online vs. Offline access [here](https://help.shopify.com/api/getting-started/authentication/oauth).
39
+
40
+ ## Access scopes
41
+
42
+ 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:
43
+
44
+ ### `ShopifyApp::ShopSessionStorageWithScopes`
45
+ ```ruby
46
+ class Shop < ActiveRecord::Base
47
+ include ShopifyApp::ShopSessionStorageWithScopes
48
+
49
+ def access_scopes=(scopes)
50
+ # Store access scopes
51
+ end
52
+ def access_scopes
53
+ # Find access scopes
54
+ end
55
+ end
56
+ ```
57
+
58
+ ### `ShopifyApp::UserSessionStorageWithScopes`
59
+ ```ruby
60
+ class User < ActiveRecord::Base
61
+ include ShopifyApp::UserSessionStorageWithScopes
62
+
63
+ def access_scopes=(scopes)
64
+ # Store access scopes
65
+ end
66
+ def access_scopes
67
+ # Find access scopes
68
+ end
69
+ end
70
+ ```
71
+ ## Migrating from shop-based to user-based token strategy
72
+
73
+ 1. Run the `user_model` generator as mentioned above.
74
+ 2. Ensure that both your `Shop` model and `User` model includes the necessary concerns `ShopifyApp::ShopSessionStorage` and `ShopifyApp::UserSessionStorage`.
75
+ 3. Make changes to 2 initializer files as shown below:
76
+ ```ruby
77
+ # In the `omniauth.rb` initializer:
78
+ provider :shopify,
79
+ ...
80
+ setup: lambda { |env|
81
+ configuration = ShopifyApp::OmniAuthConfiguration.new(env['omniauth.strategy'], Rack::Request.new(env))
82
+ configuration.build_options
83
+ }
84
+
85
+ # In the `shopify_app.rb` initializer:
86
+ config.shop_session_repository = {YOUR_SHOP_MODEL_CLASS}
87
+ config.user_session_repository = {YOUR_USER_MODEL_CLASS}
88
+ ```
@@ -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.
@@ -6,16 +6,15 @@ module ShopifyApp
6
6
  class HomeControllerGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path('../templates', __FILE__)
8
8
 
9
- class_option :with_session_token, type: :boolean, default: false
9
+ class_option :with_cookie_authentication, type: :boolean, default: false
10
+ class_option :embedded, type: :string, default: 'true'
10
11
 
11
12
  def create_home_controller
12
- @with_session_token = options['with_session_token']
13
-
14
13
  template(home_controller_template, 'app/controllers/home_controller.rb')
15
14
  end
16
15
 
17
16
  def create_products_controller
18
- generate("shopify_app:products_controller") if with_session_token?
17
+ generate("shopify_app:products_controller") unless with_cookie_authentication?
19
18
  end
20
19
 
21
20
  def create_home_index_view
@@ -28,16 +27,26 @@ module ShopifyApp
28
27
 
29
28
  private
30
29
 
30
+ def embedded?
31
+ options['embedded'] == 'true'
32
+ end
33
+
31
34
  def embedded_app?
32
35
  ShopifyApp.configuration.embedded_app?
33
36
  end
34
37
 
35
- def with_session_token?
36
- @with_session_token
38
+ def with_cookie_authentication?
39
+ options['with_cookie_authentication']
37
40
  end
38
41
 
39
42
  def home_controller_template
40
- with_session_token? ? 'unauthenticated_home_controller.rb' : 'home_controller.rb'
43
+ return 'unauthenticated_home_controller.rb' unless authenticated_home_controller_required?
44
+
45
+ 'home_controller.rb'
46
+ end
47
+
48
+ def authenticated_home_controller_required?
49
+ with_cookie_authentication? || !embedded? || !embedded_app?
41
50
  end
42
51
  end
43
52
  end
@@ -1,8 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HomeController < AuthenticatedController
4
+ include ShopifyApp::ShopAccessScopesVerification
5
+
6
+ before_action :set_host
7
+
4
8
  def index
5
9
  @products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
6
10
  @webhooks = ShopifyAPI::Webhook.find(:all)
7
11
  end
12
+
13
+ private
14
+
15
+ def set_host
16
+ @host = params[:host]
17
+ end
8
18
  end
@@ -7,7 +7,7 @@
7
7
  rel="stylesheet"
8
8
  href="https://unpkg.com/@shopify/polaris@4.25.0/styles.min.css"
9
9
  />
10
- <% if @with_session_token %> <script>
10
+ <% unless with_cookie_authentication? %> <script>
11
11
  document.addEventListener("DOMContentLoaded", async function() {
12
12
  var SessionToken = window["app-bridge"].actions.SessionToken
13
13
  var app = window.app;
@@ -18,7 +18,7 @@
18
18
 
19
19
  // Save a session token for future requests
20
20
  window.sessionToken = await new Promise((resolve) => {
21
- app.subscribe(SessionToken.ActionType.RESPOND, (data) => {
21
+ app.subscribe(SessionToken.Action.RESPOND, (data) => {
22
22
  resolve(data.sessionToken || "");
23
23
  });
24
24
  });
@@ -47,7 +47,7 @@
47
47
  <% end %> </head>
48
48
  <body>
49
49
  <h2>Products</h2>
50
- <% if @with_session_token %> <div id="products"><br>Loading...</div><% else %>
50
+ <% unless with_cookie_authentication? %> <div id="products"><br>Loading...</div><% else %>
51
51
  <ul>
52
52
  <%% @products.each do |product| %>
53
53
  <li><%%= link_to product.title, "https://#{@current_shopify_session.domain}/admin/products/#{product.id}", target: "_top" %></li>
@@ -55,17 +55,17 @@
55
55
  </ul>
56
56
 
57
57
  <hr>
58
-
58
+ <% end %>
59
59
  <h2>Webhooks</h2>
60
60
 
61
61
  <%% if @webhooks.present? %>
62
- <ul>
63
- <%% @webhooks.each do |webhook| %>
64
- <li><%%= webhook.topic %> : <%%= webhook.address %></li>
65
- <%% end %>
66
- </ul>
62
+ <ul>
63
+ <%% @webhooks.each do |webhook| %>
64
+ <li><%%= webhook.topic %> : <%%= webhook.address %></li>
65
+ <%% end %>
66
+ </ul>
67
67
  <%% else %>
68
- <p>This app has not created any webhooks for this Shop. Add webhooks to your ShopifyApp initializer if you need webhooks</p>
69
- <%% end %><% end %>
68
+ <p>This app has not created any webhooks for this Shop. Add webhooks to your ShopifyApp initializer if you need webhooks</p>
69
+ <%% end %>
70
70
  </body>
71
71
  </html>
@@ -3,8 +3,10 @@
3
3
  class HomeController < ApplicationController
4
4
  include ShopifyApp::EmbeddedApp
5
5
  include ShopifyApp::RequireKnownShop
6
+ include ShopifyApp::ShopAccessScopesVerification
6
7
 
7
8
  def index
8
9
  @shop_origin = current_shopify_domain
10
+ @host = params[:host]
9
11
  end
10
12
  end
@@ -11,6 +11,7 @@ module ShopifyApp
11
11
  class_option :scope, type: :array, default: ['read_products']
12
12
  class_option :embedded, type: :string, default: 'true'
13
13
  class_option :api_version, type: :string, default: nil
14
+ class_option :with_cookie_authentication, type: :boolean, default: false
14
15
 
15
16
  def create_shopify_app_initializer
16
17
  @application_name = format_array_argument(options['application_name'])
@@ -29,9 +30,11 @@ module ShopifyApp
29
30
  copy_file('omniauth.rb', 'config/initializers/omniauth.rb')
30
31
  end
31
32
 
33
+ return if !Rails.env.test? && shopify_provider_exists?
34
+
32
35
  inject_into_file(
33
36
  'config/initializers/omniauth.rb',
34
- File.read(File.expand_path(find_in_source_paths('shopify_provider.rb'))),
37
+ shopify_provider_template,
35
38
  after: "Rails.application.config.middleware.use(OmniAuth::Builder) do\n"
36
39
  )
37
40
  end
@@ -64,13 +67,40 @@ module ShopifyApp
64
67
  def insert_hosts_into_development_config
65
68
  inject_into_file(
66
69
  'config/environments/development.rb',
67
- " config.hosts = (config.hosts rescue []) << /\\h+.ngrok.io/\n",
70
+ " config.hosts = (config.hosts rescue []) << /\\w+\\.ngrok\\.io/\n",
68
71
  after: "Rails.application.configure do\n"
69
72
  )
70
73
  end
71
74
 
72
75
  private
73
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
+
74
104
  def embedded_app?
75
105
  options['embedded'] == 'true'
76
106
  end
@@ -78,6 +108,10 @@ module ShopifyApp
78
108
  def format_array_argument(array)
79
109
  array.join(' ').tr('"', '')
80
110
  end
111
+
112
+ def with_cookie_authentication?
113
+ options['with_cookie_authentication'] || !embedded_app?
114
+ end
81
115
  end
82
116
  end
83
117
  end
@@ -24,11 +24,12 @@
24
24
 
25
25
  <%= render 'layouts/flash_messages' %>
26
26
 
27
- <script src="https://unpkg.com/@shopify/app-bridge"></script>
27
+ <script src="https://unpkg.com/@shopify/app-bridge@2"></script>
28
28
 
29
29
  <%= content_tag(:div, nil, id: 'shopify-app-init', data: {
30
30
  api_key: ShopifyApp.configuration.api_key,
31
31
  shop_origin: @shop_origin || (@current_shopify_session.domain if @current_shopify_session),
32
+ host: @host,
32
33
  debug: Rails.env.development?
33
34
  } ) %>
34
35
 
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  Rails.application.config.middleware.use(OmniAuth::Builder) do
3
4
  end
@@ -4,7 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
4
4
  var createApp = AppBridge.default;
5
5
  window.app = createApp({
6
6
  apiKey: data.apiKey,
7
- shopOrigin: data.shopOrigin,
7
+ host: data.host,
8
8
  });
9
9
 
10
10
  var actions = AppBridge.actions;
@@ -1,15 +1,24 @@
1
1
  ShopifyApp.configure do |config|
2
2
  config.application_name = "<%= @application_name %>"
3
- config.api_key = ENV['SHOPIFY_API_KEY']
4
- config.secret = ENV['SHOPIFY_API_SECRET']
5
3
  config.old_secret = "<%= @old_secret %>"
6
4
  config.scope = "<%= @scope %>" # Consult this page for more scope options:
7
- # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
5
+ # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
8
6
  config.embedded_app = <%= embedded_app? %>
9
7
  config.after_authenticate_job = false
10
8
  config.api_version = "<%= @api_version %>"
11
9
  config.shop_session_repository = 'Shop'
12
- config.allow_jwt_authentication = true
10
+
11
+ config.reauth_on_access_scope_changes = true
12
+
13
+ config.allow_jwt_authentication = <%= !with_cookie_authentication? %>
14
+ config.allow_cookie_authentication = <%= with_cookie_authentication? %>
15
+
16
+ config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence
17
+ config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence
18
+ if defined? Rails::Server
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
21
+ end
13
22
  end
14
23
 
15
24
  # ShopifyApp::Utils.fetch_known_api_versions # Uncomment to fetch known api versions from shopify servers on boot
@@ -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
@@ -0,0 +1,5 @@
1
+ class AddShopAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def change
3
+ add_column :shops, :access_scopes, :string
4
+ end
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class Shop < ActiveRecord::Base
3
- include ShopifyApp::ShopSessionStorage
3
+ include ShopifyApp::ShopSessionStorageWithScopes
4
4
 
5
5
  def api_version
6
6
  ShopifyApp.configuration.api_version
@@ -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
@@ -0,0 +1,5 @@
1
+ class AddUserAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def change
3
+ add_column :users, :access_scopes, :string
4
+ end
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class User < ActiveRecord::Base
3
- include ShopifyApp::UserSessionStorage
3
+ include ShopifyApp::UserSessionStorageWithScopes
4
4
 
5
5
  def api_version
6
6
  ShopifyApp.configuration.api_version
@@ -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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module AccessScopes
5
+ class NoopStrategy
6
+ class << self
7
+ def update_access_scopes?(*_args)
8
+ false
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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