shopify_app 13.2.0 → 20.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 +22 -0
- data/.github/workflows/build.yml +40 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/close-waiting-for-response-issues.yml +20 -0
- data/.github/workflows/release.yml +24 -0
- data/.github/workflows/remove-labels-on-activity.yml +16 -0
- data/.github/workflows/rubocop.yml +22 -0
- data/.github/workflows/stale.yml +31 -0
- data/.gitignore +1 -2
- data/.nvmrc +1 -1
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +221 -0
- data/CONTRIBUTING.md +81 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +248 -0
- data/README.md +74 -563
- data/Rakefile +4 -3
- data/SECURITY.md +59 -0
- data/app/assets/images/storage_access.svg +1 -2
- data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +10 -0
- data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
- data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +1 -0
- data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
- data/app/assets/javascripts/shopify_app/redirect.js +10 -14
- data/app/assets/javascripts/shopify_app/storage_access.js +5 -10
- data/app/assets/javascripts/shopify_app/top_level_interaction.js +1 -1
- data/app/controllers/concerns/shopify_app/authenticated.rb +4 -0
- data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +39 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +48 -0
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +40 -0
- data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
- data/app/controllers/shopify_app/callback_controller.rb +56 -77
- data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
- data/app/controllers/shopify_app/sessions_controller.rb +33 -117
- data/app/controllers/shopify_app/webhooks_controller.rb +5 -26
- 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 +2 -7
- data/app/views/shopify_app/sessions/new.html.erb +38 -110
- data/app/views/shopify_app/sessions/request_storage_access.html.erb +12 -12
- data/app/views/shopify_app/sessions/top_level_interaction.html.erb +21 -22
- data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
- data/app/views/shopify_app/shared/redirect.html.erb +2 -2
- data/config/locales/de.yml +11 -11
- data/config/locales/ja.yml +4 -4
- data/config/locales/nl.yml +2 -2
- data/config/locales/th.yml +4 -4
- data/config/locales/vi.yml +22 -0
- data/config/locales/zh-CN.yml +2 -2
- data/config/routes.rb +20 -12
- data/docs/Quickstart.md +19 -83
- data/docs/Releasing.md +18 -15
- data/docs/Troubleshooting.md +140 -5
- data/docs/Upgrading.md +247 -0
- data/docs/shopify_app/authentication.md +128 -0
- data/docs/shopify_app/content-security-policy.md +10 -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 +24 -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/karma.conf.js +1 -1
- data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +10 -9
- data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +4 -3
- data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +15 -14
- data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +9 -1
- data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +7 -6
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +2 -1
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +1 -1
- data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +4 -4
- data/lib/generators/shopify_app/controllers/controllers_generator.rb +5 -4
- data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +27 -4
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +12 -2
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +74 -16
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +16 -0
- data/lib/generators/shopify_app/install/install_generator.rb +52 -40
- data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +5 -2
- data/lib/generators/shopify_app/install/templates/flash_messages.js +0 -2
- data/lib/generators/shopify_app/install/templates/session_store.rb +2 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.js +1 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +43 -5
- data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
- data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
- data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +4 -4
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
- data/lib/generators/shopify_app/routes/routes_generator.rb +6 -5
- data/lib/generators/shopify_app/routes/templates/routes.rb +5 -5
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +35 -7
- 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 +2 -1
- data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
- 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 +2 -1
- data/lib/generators/shopify_app/user_model/user_model_generator.rb +35 -7
- data/lib/generators/shopify_app/views/views_generator.rb +5 -4
- 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 +58 -11
- data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +4 -4
- data/lib/shopify_app/controller_concerns/csrf_protection.rb +16 -0
- data/lib/shopify_app/controller_concerns/embedded_app.rb +6 -3
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +243 -0
- data/lib/shopify_app/controller_concerns/frame_ancestors.rb +16 -0
- data/lib/shopify_app/controller_concerns/itp.rb +3 -3
- data/lib/shopify_app/controller_concerns/localization.rb +1 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +105 -90
- data/lib/shopify_app/controller_concerns/payload_verification.rb +25 -0
- data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +36 -0
- data/lib/shopify_app/controller_concerns/sanitized_params.rb +36 -0
- data/lib/shopify_app/controller_concerns/webhook_verification.rb +3 -18
- data/lib/shopify_app/engine.rb +26 -11
- data/lib/shopify_app/errors.rb +34 -0
- data/lib/shopify_app/jobs/scripttags_manager_job.rb +2 -2
- data/lib/shopify_app/jobs/webhooks_manager_job.rb +4 -5
- data/lib/shopify_app/managers/scripttags_manager.rb +12 -6
- data/lib/shopify_app/managers/webhooks_manager.rb +62 -42
- data/lib/shopify_app/middleware/jwt_middleware.rb +6 -3
- data/lib/shopify_app/session/in_memory_session_store.rb +2 -3
- data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -7
- data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -7
- data/lib/shopify_app/session/jwt.rb +19 -16
- data/lib/shopify_app/session/null_user_session_store.rb +2 -1
- data/lib/shopify_app/session/session_repository.rb +40 -2
- data/lib/shopify_app/session/session_storage.rb +4 -6
- data/lib/shopify_app/session/shop_session_storage.rb +6 -6
- data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +57 -0
- data/lib/shopify_app/session/user_session_storage.rb +20 -7
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +71 -0
- data/lib/shopify_app/test_helpers/all.rb +2 -1
- data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +4 -3
- data/lib/shopify_app/utils.rb +14 -7
- data/lib/shopify_app/version.rb +2 -1
- data/lib/shopify_app.rb +52 -29
- data/package.json +7 -8
- data/service.yml +1 -5
- data/shopify_app.gemspec +22 -20
- data/translation.yml +1 -1
- data/yarn.lock +2173 -2206
- metadata +110 -56
- data/.github/ISSUE_TEMPLATE.md +0 -14
- data/.github/probots.yml +0 -2
- data/.travis.yml +0 -28
- data/config/locales/hi.yml +0 -23
- data/config/locales/ms.yml +0 -22
- data/docs/install-on-dev-shop.png +0 -0
- data/docs/test-your-app.png +0 -0
- data/lib/generators/shopify_app/install/templates/omniauth.rb +0 -3
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -20
- data/lib/generators/shopify_app/install/templates/user_agent.rb +0 -6
- data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +0 -34
- data/package-lock.json +0 -7245
@@ -1,31 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
require "rails/generators/active_record"
|
4
5
|
|
5
6
|
module ShopifyApp
|
6
7
|
module Generators
|
7
8
|
class UserModelGenerator < Rails::Generators::Base
|
8
9
|
include Rails::Generators::Migration
|
9
|
-
source_root File.expand_path(
|
10
|
+
source_root File.expand_path("../templates", __FILE__)
|
11
|
+
|
12
|
+
class_option :new_shopify_cli_app, type: :boolean, default: false
|
10
13
|
|
11
14
|
def create_user_model
|
12
|
-
copy_file(
|
15
|
+
copy_file("user.rb", "app/models/user.rb")
|
13
16
|
end
|
14
17
|
|
15
18
|
def create_user_migration
|
16
|
-
migration_template(
|
19
|
+
migration_template("db/migrate/create_users.erb", "db/migrate/create_users.rb")
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_scopes_storage_in_user_model
|
23
|
+
scopes_column_prompt = <<~PROMPT
|
24
|
+
It is highly recommended that apps record the access scopes granted by \
|
25
|
+
merchants during app installation. See app/models/user.rb to modify how \
|
26
|
+
access scopes are stored and retrieved.
|
27
|
+
|
28
|
+
[WARNING] You will need to update the access_scopes accessors in the User model \
|
29
|
+
to allow shopify_app to store and retrieve scopes when going through OAuth.
|
30
|
+
|
31
|
+
The following migration will add an `access_scopes` column to the User model. \
|
32
|
+
Do you want to include this migration? [y/n]
|
33
|
+
PROMPT
|
34
|
+
|
35
|
+
if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
|
36
|
+
migration_template(
|
37
|
+
"db/migrate/add_user_access_scopes_column.erb",
|
38
|
+
"db/migrate/add_user_access_scopes_column.rb"
|
39
|
+
)
|
40
|
+
end
|
17
41
|
end
|
18
42
|
|
19
43
|
def update_shopify_app_initializer
|
20
|
-
gsub_file(
|
44
|
+
gsub_file("config/initializers/shopify_app.rb", "ShopifyApp::InMemoryUserSessionStore", "User")
|
21
45
|
end
|
22
46
|
|
23
47
|
def create_user_fixtures
|
24
|
-
copy_file(
|
48
|
+
copy_file("users.yml", "test/fixtures/users.yml")
|
25
49
|
end
|
26
50
|
|
27
51
|
private
|
28
52
|
|
53
|
+
def new_shopify_cli_app?
|
54
|
+
options["new_shopify_cli_app"]
|
55
|
+
end
|
56
|
+
|
29
57
|
def rails_migration_version
|
30
58
|
Rails.version.match(/\d\.\d/)[0]
|
31
59
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
3
4
|
|
4
5
|
module ShopifyApp
|
5
6
|
module Generators
|
@@ -8,21 +9,21 @@ module ShopifyApp
|
|
8
9
|
|
9
10
|
def create_views
|
10
11
|
views.each do |view|
|
11
|
-
copy_file
|
12
|
+
copy_file(view)
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
15
16
|
private
|
16
17
|
|
17
18
|
def views
|
18
|
-
files_within_root(
|
19
|
+
files_within_root(".", "app/views/**/*.*")
|
19
20
|
end
|
20
21
|
|
21
22
|
def files_within_root(prefix, glob)
|
22
23
|
root = "#{self.class.source_root}/#{prefix}"
|
23
24
|
|
24
25
|
Dir["#{root}/#{glob}"].sort.map do |full_path|
|
25
|
-
full_path.sub(root,
|
26
|
+
full_path.sub(root, ".").gsub("/./", "/")
|
26
27
|
end
|
27
28
|
end
|
28
29
|
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)&.scope
|
16
|
+
end
|
17
|
+
|
18
|
+
def configuration_access_scopes
|
19
|
+
ShopifyAPI::Auth::AuthScopes.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 << self
|
7
|
+
def update_access_scopes?(user_id: nil, shopify_user_id: nil)
|
8
|
+
return update_access_scopes_for_user_id?(user_id) if user_id
|
9
|
+
return update_access_scopes_for_shopify_user_id?(shopify_user_id) if shopify_user_id
|
10
|
+
|
11
|
+
raise(::ShopifyApp::InvalidInput,
|
12
|
+
"#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)&.scope
|
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)&.scope
|
33
|
+
end
|
34
|
+
|
35
|
+
def configuration_access_scopes
|
36
|
+
ShopifyAPI::Auth::AuthScopes.new(ShopifyApp.configuration.user_access_scopes)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
class Configuration
|
4
5
|
# Shopify App settings. These values should match the configuration
|
@@ -9,6 +10,8 @@ module ShopifyApp
|
|
9
10
|
attr_accessor :secret
|
10
11
|
attr_accessor :old_secret
|
11
12
|
attr_accessor :scope
|
13
|
+
attr_writer :shop_access_scopes
|
14
|
+
attr_writer :user_access_scopes
|
12
15
|
attr_accessor :embedded_app
|
13
16
|
alias_method :embedded_app?, :embedded_app
|
14
17
|
attr_accessor :webhooks
|
@@ -16,9 +19,13 @@ module ShopifyApp
|
|
16
19
|
attr_accessor :after_authenticate_job
|
17
20
|
attr_accessor :api_version
|
18
21
|
|
22
|
+
attr_accessor :reauth_on_access_scope_changes
|
23
|
+
|
19
24
|
# customise urls
|
20
25
|
attr_accessor :root_url
|
21
26
|
attr_writer :login_url
|
27
|
+
attr_writer :login_callback_url
|
28
|
+
attr_accessor :embedded_redirect_url
|
22
29
|
|
23
30
|
# customise ActiveJob queue names
|
24
31
|
attr_accessor :scripttags_manager_queue_name
|
@@ -33,22 +40,24 @@ module ShopifyApp
|
|
33
40
|
# allow namespacing webhook jobs
|
34
41
|
attr_accessor :webhook_jobs_namespace
|
35
42
|
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
# allow enabling jwt headers for authentication
|
40
|
-
attr_accessor :allow_jwt_authentication
|
43
|
+
# takes a ShopifyApp::BillingConfiguration object
|
44
|
+
attr_accessor :billing
|
41
45
|
|
42
46
|
def initialize
|
43
|
-
@root_url =
|
44
|
-
@myshopify_domain =
|
47
|
+
@root_url = "/"
|
48
|
+
@myshopify_domain = "myshopify.com"
|
45
49
|
@scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
|
46
50
|
@webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
|
47
|
-
@disable_webpacker = ENV[
|
51
|
+
@disable_webpacker = ENV["SHOPIFY_APP_DISABLE_WEBPACKER"].present?
|
48
52
|
end
|
49
53
|
|
50
54
|
def login_url
|
51
|
-
@login_url || File.join(@root_url,
|
55
|
+
@login_url || File.join(@root_url, "login")
|
56
|
+
end
|
57
|
+
|
58
|
+
def login_callback_url
|
59
|
+
# Not including @root_url to keep historic behaviour
|
60
|
+
@login_callback_url || File.join("auth/shopify/callback")
|
52
61
|
end
|
53
62
|
|
54
63
|
def user_session_repository=(klass)
|
@@ -67,6 +76,18 @@ module ShopifyApp
|
|
67
76
|
ShopifyApp::SessionRepository.shop_storage
|
68
77
|
end
|
69
78
|
|
79
|
+
def shop_access_scopes_strategy
|
80
|
+
return ShopifyApp::AccessScopes::NoopStrategy unless reauth_on_access_scope_changes
|
81
|
+
|
82
|
+
ShopifyApp::AccessScopes::ShopStrategy
|
83
|
+
end
|
84
|
+
|
85
|
+
def user_access_scopes_strategy
|
86
|
+
return ShopifyApp::AccessScopes::NoopStrategy unless reauth_on_access_scope_changes
|
87
|
+
|
88
|
+
ShopifyApp::AccessScopes::UserStrategy
|
89
|
+
end
|
90
|
+
|
70
91
|
def has_webhooks?
|
71
92
|
webhooks.present?
|
72
93
|
end
|
@@ -75,8 +96,34 @@ module ShopifyApp
|
|
75
96
|
scripttags.present?
|
76
97
|
end
|
77
98
|
|
78
|
-
def
|
79
|
-
|
99
|
+
def requires_billing?
|
100
|
+
billing.present?
|
101
|
+
end
|
102
|
+
|
103
|
+
def shop_access_scopes
|
104
|
+
@shop_access_scopes || scope
|
105
|
+
end
|
106
|
+
|
107
|
+
def user_access_scopes
|
108
|
+
@user_access_scopes || scope
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class BillingConfiguration
|
113
|
+
INTERVAL_ONE_TIME = "ONE_TIME"
|
114
|
+
INTERVAL_EVERY_30_DAYS = "EVERY_30_DAYS"
|
115
|
+
INTERVAL_ANNUAL = "ANNUAL"
|
116
|
+
|
117
|
+
attr_reader :charge_name
|
118
|
+
attr_reader :amount
|
119
|
+
attr_reader :currency_code
|
120
|
+
attr_reader :interval
|
121
|
+
|
122
|
+
def initialize(charge_name:, amount:, interval:, currency_code: "USD")
|
123
|
+
@charge_name = charge_name
|
124
|
+
@amount = amount
|
125
|
+
@currency_code = currency_code
|
126
|
+
@interval = interval
|
80
127
|
end
|
81
128
|
end
|
82
129
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module AppProxyVerification
|
4
5
|
extend ActiveSupport::Concern
|
5
|
-
|
6
6
|
included do
|
7
7
|
skip_before_action :verify_authenticity_token, raise: false
|
8
8
|
before_action :verify_proxy_request
|
@@ -17,7 +17,7 @@ module ShopifyApp
|
|
17
17
|
def query_string_valid?(query_string)
|
18
18
|
query_hash = Rack::Utils.parse_query(query_string)
|
19
19
|
|
20
|
-
signature = query_hash.delete(
|
20
|
+
signature = query_hash.delete("signature")
|
21
21
|
return false if signature.nil?
|
22
22
|
|
23
23
|
ActiveSupport::SecurityUtils.secure_compare(
|
@@ -27,10 +27,10 @@ module ShopifyApp
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def calculated_signature(query_hash_without_signature)
|
30
|
-
sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(
|
30
|
+
sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(",")}" }.sort.join
|
31
31
|
|
32
32
|
OpenSSL::HMAC.hexdigest(
|
33
|
-
OpenSSL::Digest.new(
|
33
|
+
OpenSSL::Digest.new("sha256"),
|
34
34
|
ShopifyApp.configuration.secret,
|
35
35
|
sorted_params
|
36
36
|
)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module CsrfProtection
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
protect_from_forgery with: :exception, unless: :valid_session_token?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def valid_session_token?
|
13
|
+
request.env["jwt.shopify_domain"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,20 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module EmbeddedApp
|
4
5
|
extend ActiveSupport::Concern
|
5
6
|
|
7
|
+
include ShopifyApp::FrameAncestors
|
8
|
+
|
6
9
|
included do
|
7
10
|
if ShopifyApp.configuration.embedded_app?
|
8
11
|
after_action(:set_esdk_headers)
|
9
|
-
layout(
|
12
|
+
layout("embedded_app")
|
10
13
|
end
|
11
14
|
end
|
12
15
|
|
13
16
|
private
|
14
17
|
|
15
18
|
def set_esdk_headers
|
16
|
-
response.set_header(
|
17
|
-
response.headers.except!(
|
19
|
+
response.set_header("P3P", 'CP="Not used"')
|
20
|
+
response.headers.except!("X-Frame-Options")
|
18
21
|
end
|
19
22
|
end
|
20
23
|
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module EnsureBilling
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
RECURRING_INTERVALS = [BillingConfiguration::INTERVAL_EVERY_30_DAYS, BillingConfiguration::INTERVAL_ANNUAL]
|
8
|
+
|
9
|
+
included do
|
10
|
+
before_action :check_billing, if: :billing_required?
|
11
|
+
rescue_from ::ShopifyApp::BillingError, with: :handle_billing_error
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def check_billing(session = current_shopify_session)
|
17
|
+
return true if session.blank? || !billing_required?
|
18
|
+
|
19
|
+
confirmation_url = nil
|
20
|
+
|
21
|
+
if has_active_payment?(session)
|
22
|
+
has_payment = true
|
23
|
+
else
|
24
|
+
has_payment = false
|
25
|
+
confirmation_url = request_payment(session)
|
26
|
+
end
|
27
|
+
|
28
|
+
unless has_payment
|
29
|
+
if request.xhr?
|
30
|
+
add_top_level_redirection_headers(url: confirmation_url, ignore_response_code: true)
|
31
|
+
head(:unauthorized)
|
32
|
+
else
|
33
|
+
redirect_to(confirmation_url, allow_other_host: true)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
has_payment
|
38
|
+
end
|
39
|
+
|
40
|
+
def billing_required?
|
41
|
+
ShopifyApp.configuration.requires_billing?
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_billing_error(error)
|
45
|
+
logger.info("#{error.message}: #{error.errors}")
|
46
|
+
redirect_to_login
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_active_payment?(session)
|
50
|
+
if recurring?
|
51
|
+
has_subscription?(session)
|
52
|
+
else
|
53
|
+
has_one_time_payment?(session)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def has_subscription?(session)
|
58
|
+
response = run_query(session: session, query: RECURRING_PURCHASES_QUERY)
|
59
|
+
subscriptions = response.body["data"]["currentAppInstallation"]["activeSubscriptions"]
|
60
|
+
|
61
|
+
subscriptions.each do |subscription|
|
62
|
+
if subscription["name"] == ShopifyApp.configuration.billing.charge_name &&
|
63
|
+
(!Rails.env.production? || !subscription["test"])
|
64
|
+
|
65
|
+
return true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_one_time_payment?(session)
|
73
|
+
purchases = nil
|
74
|
+
end_cursor = nil
|
75
|
+
|
76
|
+
loop do
|
77
|
+
response = run_query(session: session, query: ONE_TIME_PURCHASES_QUERY, variables: { endCursor: end_cursor })
|
78
|
+
purchases = response.body["data"]["currentAppInstallation"]["oneTimePurchases"]
|
79
|
+
|
80
|
+
purchases["edges"].each do |purchase|
|
81
|
+
node = purchase["node"]
|
82
|
+
|
83
|
+
if node["name"] == ShopifyApp.configuration.billing.charge_name &&
|
84
|
+
(!Rails.env.production? || !node["test"]) &&
|
85
|
+
node["status"] == "ACTIVE"
|
86
|
+
|
87
|
+
return true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end_cursor = purchases["pageInfo"]["endCursor"]
|
92
|
+
break unless purchases["pageInfo"]["hasNextPage"]
|
93
|
+
end
|
94
|
+
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def request_payment(session)
|
99
|
+
shop = session.shop
|
100
|
+
host = Base64.encode64("#{shop}/admin")
|
101
|
+
return_url = "https://#{ShopifyAPI::Context.host_name}?shop=#{shop}&host=#{host}"
|
102
|
+
|
103
|
+
if recurring?
|
104
|
+
data = request_recurring_payment(session: session, return_url: return_url)
|
105
|
+
data = data["data"]["appSubscriptionCreate"]
|
106
|
+
else
|
107
|
+
data = request_one_time_payment(session: session, return_url: return_url)
|
108
|
+
data = data["data"]["appPurchaseOneTimeCreate"]
|
109
|
+
end
|
110
|
+
|
111
|
+
raise BillingError.new("Error while billing the store", data["userErrors"]) unless data["userErrors"].empty?
|
112
|
+
|
113
|
+
data["confirmationUrl"]
|
114
|
+
end
|
115
|
+
|
116
|
+
def request_recurring_payment(session:, return_url:)
|
117
|
+
response = run_query(
|
118
|
+
session: session,
|
119
|
+
query: RECURRING_PURCHASE_MUTATION,
|
120
|
+
variables: {
|
121
|
+
name: ShopifyApp.configuration.billing.charge_name,
|
122
|
+
lineItems: {
|
123
|
+
plan: {
|
124
|
+
appRecurringPricingDetails: {
|
125
|
+
interval: ShopifyApp.configuration.billing.interval,
|
126
|
+
price: {
|
127
|
+
amount: ShopifyApp.configuration.billing.amount,
|
128
|
+
currencyCode: ShopifyApp.configuration.billing.currency_code,
|
129
|
+
},
|
130
|
+
},
|
131
|
+
},
|
132
|
+
},
|
133
|
+
returnUrl: return_url,
|
134
|
+
test: !Rails.env.production?,
|
135
|
+
}
|
136
|
+
)
|
137
|
+
|
138
|
+
response.body
|
139
|
+
end
|
140
|
+
|
141
|
+
def request_one_time_payment(session:, return_url:)
|
142
|
+
response = run_query(
|
143
|
+
session: session,
|
144
|
+
query: ONE_TIME_PURCHASE_MUTATION,
|
145
|
+
variables: {
|
146
|
+
name: ShopifyApp.configuration.billing.charge_name,
|
147
|
+
price: {
|
148
|
+
amount: ShopifyApp.configuration.billing.amount,
|
149
|
+
currencyCode: ShopifyApp.configuration.billing.currency_code,
|
150
|
+
},
|
151
|
+
returnUrl: return_url,
|
152
|
+
test: !Rails.env.production?,
|
153
|
+
}
|
154
|
+
)
|
155
|
+
|
156
|
+
response.body
|
157
|
+
end
|
158
|
+
|
159
|
+
def recurring?
|
160
|
+
RECURRING_INTERVALS.include?(ShopifyApp.configuration.billing.interval)
|
161
|
+
end
|
162
|
+
|
163
|
+
def run_query(session:, query:, variables: nil)
|
164
|
+
client = ShopifyAPI::Clients::Graphql::Admin.new(session: session)
|
165
|
+
|
166
|
+
response = client.query(query: query, variables: variables)
|
167
|
+
|
168
|
+
raise BillingError.new("Error while billing the store", []) unless response.ok?
|
169
|
+
raise BillingError.new("Error while billing the store", response.body["errors"]) if response.body["errors"]
|
170
|
+
|
171
|
+
response
|
172
|
+
end
|
173
|
+
|
174
|
+
RECURRING_PURCHASES_QUERY = <<~'QUERY'
|
175
|
+
query appSubscription {
|
176
|
+
currentAppInstallation {
|
177
|
+
activeSubscriptions {
|
178
|
+
name, test
|
179
|
+
}
|
180
|
+
}
|
181
|
+
}
|
182
|
+
QUERY
|
183
|
+
|
184
|
+
ONE_TIME_PURCHASES_QUERY = <<~'QUERY'
|
185
|
+
query appPurchases($endCursor: String) {
|
186
|
+
currentAppInstallation {
|
187
|
+
oneTimePurchases(first: 250, sortKey: CREATED_AT, after: $endCursor) {
|
188
|
+
edges {
|
189
|
+
node {
|
190
|
+
name, test, status
|
191
|
+
}
|
192
|
+
}
|
193
|
+
pageInfo {
|
194
|
+
hasNextPage, endCursor
|
195
|
+
}
|
196
|
+
}
|
197
|
+
}
|
198
|
+
}
|
199
|
+
QUERY
|
200
|
+
|
201
|
+
RECURRING_PURCHASE_MUTATION = <<~'QUERY'
|
202
|
+
mutation createPaymentMutation(
|
203
|
+
$name: String!
|
204
|
+
$lineItems: [AppSubscriptionLineItemInput!]!
|
205
|
+
$returnUrl: URL!
|
206
|
+
$test: Boolean
|
207
|
+
) {
|
208
|
+
appSubscriptionCreate(
|
209
|
+
name: $name
|
210
|
+
lineItems: $lineItems
|
211
|
+
returnUrl: $returnUrl
|
212
|
+
test: $test
|
213
|
+
) {
|
214
|
+
confirmationUrl
|
215
|
+
userErrors {
|
216
|
+
field, message
|
217
|
+
}
|
218
|
+
}
|
219
|
+
}
|
220
|
+
QUERY
|
221
|
+
|
222
|
+
ONE_TIME_PURCHASE_MUTATION = <<~'QUERY'
|
223
|
+
mutation createPaymentMutation(
|
224
|
+
$name: String!
|
225
|
+
$price: MoneyInput!
|
226
|
+
$returnUrl: URL!
|
227
|
+
$test: Boolean
|
228
|
+
) {
|
229
|
+
appPurchaseOneTimeCreate(
|
230
|
+
name: $name
|
231
|
+
price: $price
|
232
|
+
returnUrl: $returnUrl
|
233
|
+
test: $test
|
234
|
+
) {
|
235
|
+
confirmationUrl
|
236
|
+
userErrors {
|
237
|
+
field, message
|
238
|
+
}
|
239
|
+
}
|
240
|
+
}
|
241
|
+
QUERY
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module FrameAncestors
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
content_security_policy do |policy|
|
9
|
+
policy.frame_ancestors(-> do
|
10
|
+
domain_host = current_shopify_domain || "*.#{::ShopifyApp.configuration.myshopify_domain}"
|
11
|
+
"https://#{domain_host} https://admin.shopify.com"
|
12
|
+
end)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -9,15 +9,15 @@ module ShopifyApp
|
|
9
9
|
return unless ShopifyApp.configuration.embedded_app?
|
10
10
|
return unless user_agent_can_partition_cookies
|
11
11
|
|
12
|
-
session[
|
12
|
+
session["shopify.cookies_persist"] = true
|
13
13
|
end
|
14
14
|
|
15
15
|
def set_top_level_oauth_cookie
|
16
|
-
session[
|
16
|
+
session["shopify.top_level_oauth"] = true
|
17
17
|
end
|
18
18
|
|
19
19
|
def clear_top_level_oauth_cookie
|
20
|
-
session.delete(
|
20
|
+
session.delete("shopify.top_level_oauth")
|
21
21
|
end
|
22
22
|
|
23
23
|
def user_agent_is_mobile
|