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.
- 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 +41 -0
- data/.github/workflows/release.yml +24 -0
- data/.github/workflows/rubocop.yml +1 -7
- data/.gitignore +0 -2
- data/CHANGELOG.md +75 -0
- data/CONTRIBUTING.md +76 -0
- data/Gemfile.lock +268 -0
- data/README.md +73 -572
- data/app/assets/images/storage_access.svg +1 -2
- data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
- data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +26 -0
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
- data/app/controllers/shopify_app/callback_controller.rb +34 -10
- data/app/controllers/shopify_app/sessions_controller.rb +19 -20
- data/app/views/shopify_app/partials/_button_styles.html.erb +41 -36
- data/app/views/shopify_app/partials/_card_styles.html.erb +3 -3
- data/app/views/shopify_app/partials/_empty_state_styles.html.erb +28 -59
- data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
- data/app/views/shopify_app/partials/_layout_styles.html.erb +16 -1
- data/app/views/shopify_app/partials/_typography_styles.html.erb +6 -6
- data/app/views/shopify_app/sessions/enable_cookies.html.erb +1 -1
- data/app/views/shopify_app/sessions/new.html.erb +38 -110
- data/app/views/shopify_app/sessions/request_storage_access.html.erb +1 -1
- data/app/views/shopify_app/sessions/top_level_interaction.html.erb +20 -15
- data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
- data/config/locales/de.yml +11 -11
- data/config/locales/nl.yml +1 -1
- data/config/locales/vi.yml +22 -0
- data/config/locales/zh-CN.yml +1 -1
- data/docs/Quickstart.md +15 -77
- data/docs/Releasing.md +9 -6
- data/docs/Troubleshooting.md +147 -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/home_controller_generator.rb +16 -7
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +10 -0
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +11 -11
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +2 -0
- data/lib/generators/shopify_app/install/install_generator.rb +36 -2
- data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +2 -1
- data/lib/generators/shopify_app/install/templates/omniauth.rb +1 -0
- data/lib/generators/shopify_app/install/templates/shopify_app.js +1 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +13 -4
- 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/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 +25 -0
- data/lib/shopify_app/controller_concerns/itp.rb +0 -2
- data/lib/shopify_app/controller_concerns/login_protection.rb +14 -17
- data/lib/shopify_app/engine.rb +1 -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/jwt.rb +3 -1
- 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/lib/shopify_app.rb +11 -0
- data/package.json +1 -1
- data/service.yml +1 -4
- data/shopify_app.gemspec +5 -4
- data/translation.yml +1 -1
- data/yarn.lock +92 -123
- metadata +62 -16
- data/.github/ISSUE_TEMPLATE.md +0 -19
- data/.travis.yml +0 -27
- 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,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 :
|
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")
|
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
|
36
|
-
|
38
|
+
def with_cookie_authentication?
|
39
|
+
options['with_cookie_authentication']
|
37
40
|
end
|
38
41
|
|
39
42
|
def home_controller_template
|
40
|
-
|
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
|
-
<%
|
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.
|
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
|
-
<%
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
<ul>
|
63
|
+
<%% @webhooks.each do |webhook| %>
|
64
|
+
<li><%%= webhook.topic %> : <%%= webhook.address %></li>
|
65
|
+
<%% end %>
|
66
|
+
</ul>
|
67
67
|
<%% else %>
|
68
|
-
|
69
|
-
<%% 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>
|
data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb
CHANGED
@@ -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
|
-
|
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 []) << /\\
|
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,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
|
-
|
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
|
-
|
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
|
@@ -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
|
@@ -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
|