shopify_app 11.3.2 → 11.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -2
  3. data/CHANGELOG.md +24 -0
  4. data/README.md +100 -102
  5. data/app/controllers/concerns/shopify_app/authenticated.rb +1 -1
  6. data/app/controllers/shopify_app/callback_controller.rb +8 -2
  7. data/app/controllers/shopify_app/extension_verification_controller.rb +20 -0
  8. data/config/locales/nl.yml +7 -7
  9. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +39 -0
  10. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +66 -0
  11. data/lib/generators/shopify_app/install/install_generator.rb +0 -4
  12. data/lib/generators/shopify_app/install/templates/shopify_app.rb +1 -1
  13. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +1 -0
  14. data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
  15. data/lib/generators/shopify_app/user_model/templates/user.rb +7 -0
  16. data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
  17. data/lib/generators/shopify_app/user_model/user_model_generator.rb +38 -0
  18. data/lib/shopify_app.rb +5 -0
  19. data/lib/shopify_app/configuration.rb +13 -8
  20. data/lib/shopify_app/controller_concerns/login_protection.rb +22 -3
  21. data/lib/shopify_app/engine.rb +4 -0
  22. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +60 -0
  23. data/lib/shopify_app/session/in_memory_session_store.rb +1 -1
  24. data/lib/shopify_app/session/session_repository.rb +2 -2
  25. data/lib/shopify_app/session/session_storage.rb +14 -15
  26. data/lib/shopify_app/session/storage_strategies/shop_storage_strategy.rb +24 -0
  27. data/lib/shopify_app/session/storage_strategies/user_storage_strategy.rb +26 -0
  28. data/lib/shopify_app/version.rb +1 -1
  29. data/package-lock.json +33 -35
  30. data/package.json +3 -2
  31. data/service.yml +1 -1
  32. data/shopify_app.gemspec +4 -2
  33. data/yarn.lock +14 -14
  34. metadata +49 -11
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MarketingActivitiesController < ShopifyApp::ExtensionVerificationController
4
+ def preload_form_data
5
+ preload_data = {
6
+ "form_data": {
7
+ "budget": {
8
+ "currency": "USD",
9
+ }
10
+ }
11
+ }
12
+ render(json: preload_data, status: :ok)
13
+ end
14
+
15
+ def update
16
+ render(json: {}, status: :accepted)
17
+ end
18
+
19
+ def pause
20
+ render(json: {}, status: :accepted)
21
+ end
22
+
23
+ def resume
24
+ render(json: {}, status: :accepted)
25
+ end
26
+
27
+ def delete
28
+ render(json: {}, status: :accepted)
29
+ end
30
+
31
+ def preview
32
+ placeholder_img = "https://cdn.shopify.com/s/files/1/0533/2089/files/placeholder-images-image_small.png"
33
+ preview_response = {
34
+ "desktop": {
35
+ "preview_url": placeholder_img,
36
+ "content_type": "text/html",
37
+ "width": 360,
38
+ "height": 200
39
+ },
40
+ "mobile": {
41
+ "preview_url": placeholder_img,
42
+ "content_type": "text/html",
43
+ "width": 360,
44
+ "height": 200
45
+ }
46
+ }
47
+ render(json: preview_response, status: :ok)
48
+ end
49
+
50
+ def create
51
+ render(json: {}, status: :ok)
52
+ end
53
+
54
+ def republish
55
+ render(json: {}, status: :accepted)
56
+ end
57
+
58
+ def errors
59
+ request_id = params[:request_id]
60
+ message = params[:message]
61
+
62
+ Rails.logger.info("[Marketing Activity App Error Feedback] Request id: #{request_id}, message: #{message}")
63
+
64
+ render(json: {}, status: :ok)
65
+ end
66
+ end
@@ -11,10 +11,6 @@ module ShopifyApp
11
11
  class_option :embedded, type: :string, default: 'true'
12
12
  class_option :api_version, type: :string, default: nil
13
13
 
14
- def add_dotenv_gem
15
- gem('dotenv-rails', group: [:test, :development])
16
- end
17
-
18
14
  def create_shopify_app_initializer
19
15
  @application_name = format_array_argument(options['application_name'])
20
16
  @scope = format_array_argument(options['scope'])
@@ -8,7 +8,7 @@ ShopifyApp.configure do |config|
8
8
  config.embedded_app = <%= embedded_app? %>
9
9
  config.after_authenticate_job = false
10
10
  config.api_version = "<%= @api_version %>"
11
- config.session_repository = ShopifyApp::InMemorySessionStore
11
+ config.session_repository = 'ShopifyApp::InMemorySessionStore'
12
12
  end
13
13
 
14
14
  # ShopifyApp::Utils.fetch_known_api_versions # Uncomment to fetch known api versions from shopify servers on boot
@@ -4,6 +4,7 @@ provider :shopify,
4
4
  ShopifyApp.configuration.api_key,
5
5
  ShopifyApp.configuration.secret,
6
6
  scope: ShopifyApp.configuration.scope,
7
+ per_user_permissions: ShopifyApp.configuration.per_user_tokens,
7
8
  setup: lambda { |env|
8
9
  strategy = env['omniauth.strategy']
9
10
 
@@ -0,0 +1,16 @@
1
+ class CreateUsers < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.bigint :shopify_user_id, null: false
5
+ t.string :shopify_domain, null: false
6
+ t.string :shopify_token, null: false
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :users, :shopify_user_id, unique: true
11
+ end
12
+
13
+ def self.down
14
+ drop_table :users
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ class User < ActiveRecord::Base
2
+ include ShopifyApp::SessionStorage
3
+
4
+ def api_version
5
+ ShopifyApp.configuration.api_version
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ regular_user:
2
+ shopify_domain: 'regular-shop.myshopify.com'
3
+ shopify_token: 'token'
4
+ shopify_user_id: 1
@@ -0,0 +1,38 @@
1
+ require 'rails/generators/base'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ShopifyApp
5
+ module Generators
6
+ class UserModelGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def create_user_model
11
+ copy_file 'user.rb', 'app/models/user.rb'
12
+ end
13
+
14
+ def create_user_migration
15
+ migration_template 'db/migrate/create_users.erb', 'db/migrate/create_users.rb'
16
+ end
17
+
18
+ def update_shopify_app_initializer
19
+ gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemorySessionStore', 'User'
20
+ end
21
+
22
+ def create_user_fixtures
23
+ copy_file 'users.yml', 'test/fixtures/users.yml'
24
+ end
25
+
26
+ private
27
+
28
+ def rails_migration_version
29
+ Rails.version.match(/\d\.\d/)[0]
30
+ end
31
+
32
+ # for generating a timestamp when using `create_migration`
33
+ def self.next_migration_number(dir)
34
+ ActiveRecord::Generators::Base.next_migration_number(dir)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -40,7 +40,12 @@ module ShopifyApp
40
40
  require 'shopify_app/managers/webhooks_manager'
41
41
  require 'shopify_app/managers/scripttags_manager'
42
42
 
43
+ # middleware
44
+ require 'shopify_app/middleware/same_site_cookie_middleware'
45
+
43
46
  # session
47
+ require 'shopify_app/session/storage_strategies/shop_storage_strategy'
48
+ require 'shopify_app/session/storage_strategies/user_storage_strategy'
44
49
  require 'shopify_app/session/session_storage'
45
50
  require 'shopify_app/session/session_repository'
46
51
  require 'shopify_app/session/in_memory_session_store'
@@ -14,7 +14,9 @@ module ShopifyApp
14
14
  attr_accessor :webhooks
15
15
  attr_accessor :scripttags
16
16
  attr_accessor :after_authenticate_job
17
- attr_accessor :session_repository
17
+ attr_reader :session_repository
18
+ attr_accessor :per_user_tokens
19
+ alias_method :per_user_tokens?, :per_user_tokens
18
20
  attr_accessor :api_version
19
21
 
20
22
  # customise urls
@@ -34,11 +36,15 @@ module ShopifyApp
34
36
  # allow namespacing webhook jobs
35
37
  attr_accessor :webhook_jobs_namespace
36
38
 
39
+ # allow enabling of same site none on cookies
40
+ attr_accessor :enable_same_site_none
41
+
37
42
  def initialize
38
43
  @root_url = '/'
39
44
  @myshopify_domain = 'myshopify.com'
40
45
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
41
46
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
47
+ @per_user_tokens = false
42
48
  @disable_webpacker = ENV['SHOPIFY_APP_DISABLE_WEBPACKER'].present?
43
49
  end
44
50
 
@@ -47,13 +53,8 @@ module ShopifyApp
47
53
  end
48
54
 
49
55
  def session_repository=(klass)
50
- if Rails.configuration.cache_classes
51
- ShopifyApp::SessionRepository.storage = klass
52
- else
53
- ActiveSupport::Reloader.to_prepare do
54
- ShopifyApp::SessionRepository.storage = klass
55
- end
56
- end
56
+ @session_repository = klass
57
+ ShopifyApp::SessionRepository.storage = klass
57
58
  end
58
59
 
59
60
  def has_webhooks?
@@ -63,6 +64,10 @@ module ShopifyApp
63
64
  def has_scripttags?
64
65
  scripttags.present?
65
66
  end
67
+
68
+ def enable_same_site_none
69
+ @enable_same_site_none.nil? ? embedded_app? : @enable_same_site_none
70
+ end
66
71
  end
67
72
 
68
73
  def self.configuration
@@ -27,12 +27,30 @@ module ShopifyApp
27
27
  end
28
28
 
29
29
  def shop_session
30
- return unless session[:shopify]
31
- @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify])
30
+ if ShopifyApp.configuration.per_user_tokens?
31
+ return unless session[:shopify_user]
32
+ @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify_user]['id'])
33
+ else
34
+ return unless session[:shopify]
35
+ @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify])
36
+ end
32
37
  end
33
38
 
34
- def login_again_if_different_shop
39
+ def login_again_if_different_user_or_shop
40
+ if ShopifyApp.configuration.per_user_tokens?
41
+ valid_session_data = session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
42
+ sessions_do_not_match = session[:user_session] != params[:session] # current user is different from stored user
43
+
44
+ if valid_session_data && sessions_do_not_match
45
+ clear_session = true
46
+ end
47
+ end
48
+
35
49
  if shop_session && params[:shop] && params[:shop].is_a?(String) && (shop_session.domain != params[:shop])
50
+ clear_session = true
51
+ end
52
+
53
+ if clear_session
36
54
  clear_shop_session
37
55
  redirect_to_login
38
56
  end
@@ -60,6 +78,7 @@ module ShopifyApp
60
78
  session[:shopify] = nil
61
79
  session[:shopify_domain] = nil
62
80
  session[:shopify_user] = nil
81
+ session[:user_session] = nil
63
82
  end
64
83
 
65
84
  def login_url_with_optional_shop(top_level: false)
@@ -12,5 +12,9 @@ module ShopifyApp
12
12
  storage_access.svg
13
13
  ]
14
14
  end
15
+
16
+ initializer "shopify_app.middleware" do |app|
17
+ app.config.middleware.insert_before(ActionDispatch::Cookies, ShopifyApp::SameSiteCookieMiddleware)
18
+ end
15
19
  end
16
20
  end
@@ -0,0 +1,60 @@
1
+ module ShopifyApp
2
+ class SameSiteCookieMiddleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ _status, headers, _body = @app.call(env)
9
+ ensure
10
+ user_agent = env['HTTP_USER_AGENT']
11
+
12
+ if headers && headers['Set-Cookie'] && !SameSiteCookieMiddleware.same_site_none_incompatible?(user_agent) &&
13
+ ShopifyApp.configuration.enable_same_site_none
14
+
15
+ cookies = headers['Set-Cookie'].split("\n").compact
16
+
17
+ cookies.each do |cookie|
18
+ unless cookie.include?("; SameSite")
19
+ headers['Set-Cookie'] = headers['Set-Cookie'].gsub("#{cookie}", "#{cookie}; secure; SameSite=None")
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.same_site_none_incompatible?(user_agent)
26
+ sniffer = BrowserSniffer.new(user_agent)
27
+
28
+ webkit_same_site_bug?(sniffer) || drops_unrecognized_same_site_cookies?(sniffer)
29
+ rescue
30
+ true
31
+ end
32
+
33
+ def self.webkit_same_site_bug?(sniffer)
34
+ (sniffer.os == :ios && sniffer.os_version.match?(/^([0-9]|1[12])[\.\_]/)) ||
35
+ (sniffer.os == :mac && sniffer.browser == :safari && sniffer.os_version.match?(/^10[\.\_]14/))
36
+ end
37
+
38
+ def self.drops_unrecognized_same_site_cookies?(sniffer)
39
+ (chromium_based?(sniffer) && sniffer.major_browser_version >= 51 && sniffer.major_browser_version <= 66) ||
40
+ (uc_browser?(sniffer) && !uc_browser_version_at_least?(sniffer: sniffer, major: 12, minor: 13, build: 2))
41
+ end
42
+
43
+ def self.chromium_based?(sniffer)
44
+ sniffer.browser_name.downcase.match?(/chrom(e|ium)/)
45
+ end
46
+
47
+ def self.uc_browser?(sniffer)
48
+ sniffer.user_agent.downcase.match?(/uc\s?browser/)
49
+ end
50
+
51
+ def self.uc_browser_version_at_least?(sniffer:, major:, minor:, build:)
52
+ digits = sniffer.browser_version.split('.').map(&:to_i)
53
+ return false unless digits.count >= 3
54
+
55
+ return digits[0] > major if digits[0] != major
56
+ return digits[1] > minor if digits[1] != minor
57
+ digits[2] >= build
58
+ end
59
+ end
60
+ end
@@ -6,7 +6,7 @@ module ShopifyApp
6
6
  repo[id]
7
7
  end
8
8
 
9
- def self.store(session)
9
+ def self.store(session, *args)
10
10
  id = SecureRandom.uuid
11
11
  repo[id] = session
12
12
  id
@@ -15,8 +15,8 @@ module ShopifyApp
15
15
  storage.retrieve(id)
16
16
  end
17
17
 
18
- def store(session)
19
- storage.store(session)
18
+ def store(session, *args)
19
+ storage.store(session, *args)
20
20
  end
21
21
 
22
22
  def storage
@@ -3,9 +3,12 @@ module ShopifyApp
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false }
7
6
  validates :shopify_token, presence: true
8
7
  validates :api_version, presence: true
8
+ validates :shopify_domain, presence: true,
9
+ if: Proc.new {|_| ShopifyApp.configuration.per_user_tokens? }
10
+ validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false },
11
+ if: Proc.new {|_| !ShopifyApp.configuration.per_user_tokens? }
9
12
  end
10
13
 
11
14
  def with_shopify_session(&block)
@@ -18,23 +21,19 @@ module ShopifyApp
18
21
  end
19
22
 
20
23
  class_methods do
21
- def store(session)
22
- shop = find_or_initialize_by(shopify_domain: session.domain)
23
- shop.shopify_token = session.token
24
- shop.save!
25
- shop.id
24
+
25
+ def strategy_klass
26
+ ShopifyApp.configuration.per_user_tokens? ?
27
+ ShopifyApp::SessionStorage::UserStorageStrategy :
28
+ ShopifyApp::SessionStorage::ShopStorageStrategy
26
29
  end
27
30
 
28
- def retrieve(id)
29
- return unless id
31
+ def store(auth_session, user: nil)
32
+ strategy_klass.store(auth_session, user)
33
+ end
30
34
 
31
- if shop = self.find_by(id: id)
32
- ShopifyAPI::Session.new(
33
- domain: shop.shopify_domain,
34
- token: shop.shopify_token,
35
- api_version: shop.api_version
36
- )
37
- end
35
+ def retrieve(id)
36
+ strategy_klass.retrieve(id)
38
37
  end
39
38
  end
40
39
  end
@@ -0,0 +1,24 @@
1
+ module ShopifyApp
2
+ module SessionStorage
3
+ class ShopStorageStrategy
4
+
5
+ def self.store(auth_session, *args)
6
+ shop = Shop.find_or_initialize_by(shopify_domain: auth_session.domain)
7
+ shop.shopify_token = auth_session.token
8
+ shop.save!
9
+ shop.id
10
+ end
11
+
12
+ def self.retrieve(id)
13
+ return unless id
14
+ if shop = Shop.find_by(id: id)
15
+ ShopifyAPI::Session.new(
16
+ domain: shop.shopify_domain,
17
+ token: shop.shopify_token,
18
+ api_version: shop.api_version
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module ShopifyApp
2
+ module SessionStorage
3
+ class UserStorageStrategy
4
+
5
+ def self.store(auth_session, user)
6
+ user = User.find_or_initialize_by(shopify_user_id: user[:id])
7
+ user.shopify_token = auth_session.token
8
+ user.shopify_domain = auth_session.domain
9
+ user.save!
10
+ user.id
11
+ end
12
+
13
+ def self.retrieve(id)
14
+ return unless id
15
+ if user = User.find_by(shopify_user_id: id)
16
+ ShopifyAPI::Session.new(
17
+ domain: user.shopify_domain,
18
+ token: user.shopify_token,
19
+ api_version: user.api_version
20
+ )
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end