shopify_app 12.0.4 → 13.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -6
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile +3 -0
  5. data/README.md +98 -20
  6. data/Rakefile +1 -0
  7. data/app/controllers/concerns/shopify_app/authenticated.rb +1 -1
  8. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  9. data/app/controllers/shopify_app/callback_controller.rb +15 -11
  10. data/app/controllers/shopify_app/sessions_controller.rb +35 -9
  11. data/app/controllers/shopify_app/webhooks_controller.rb +6 -5
  12. data/config/locales/fi.yml +1 -1
  13. data/config/routes.rb +1 -0
  14. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +5 -3
  15. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  16. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +2 -1
  17. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +4 -4
  18. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +5 -4
  19. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb +5 -0
  20. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +4 -3
  21. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +3 -3
  22. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +10 -9
  23. data/lib/generators/shopify_app/controllers/controllers_generator.rb +1 -0
  24. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +4 -3
  25. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
  26. data/lib/generators/shopify_app/install/install_generator.rb +9 -8
  27. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +1 -1
  28. data/lib/generators/shopify_app/install/templates/omniauth.rb +2 -1
  29. data/lib/generators/shopify_app/install/templates/shopify_app.rb +1 -1
  30. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +1 -1
  31. data/lib/generators/shopify_app/install/templates/user_agent.rb +2 -1
  32. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -0
  33. data/lib/generators/shopify_app/routes/templates/routes.rb +10 -9
  34. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +5 -4
  35. data/lib/generators/shopify_app/shop_model/templates/shop.rb +2 -1
  36. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  37. data/lib/generators/shopify_app/user_model/templates/user.rb +2 -1
  38. data/lib/generators/shopify_app/user_model/user_model_generator.rb +5 -4
  39. data/lib/generators/shopify_app/views/views_generator.rb +1 -0
  40. data/lib/shopify_app.rb +9 -4
  41. data/lib/shopify_app/configuration.rb +20 -10
  42. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -2
  43. data/lib/shopify_app/controller_concerns/embedded_app.rb +3 -2
  44. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  45. data/lib/shopify_app/controller_concerns/login_protection.rb +65 -25
  46. data/lib/shopify_app/controller_concerns/webhook_verification.rb +2 -1
  47. data/lib/shopify_app/engine.rb +1 -0
  48. data/lib/shopify_app/jobs/scripttags_manager_job.rb +1 -1
  49. data/lib/shopify_app/jobs/webhooks_manager_job.rb +1 -1
  50. data/lib/shopify_app/managers/scripttags_manager.rb +4 -3
  51. data/lib/shopify_app/managers/webhooks_manager.rb +4 -3
  52. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +5 -38
  53. data/lib/shopify_app/session/in_memory_session_store.rb +7 -3
  54. data/lib/shopify_app/session/in_memory_shop_session_store.rb +14 -0
  55. data/lib/shopify_app/session/in_memory_user_session_store.rb +14 -0
  56. data/lib/shopify_app/session/jwt.rb +48 -0
  57. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  58. data/lib/shopify_app/session/session_repository.rb +36 -14
  59. data/lib/shopify_app/session/session_storage.rb +1 -10
  60. data/lib/shopify_app/session/shop_session_storage.rb +42 -0
  61. data/lib/shopify_app/session/user_session_storage.rb +42 -0
  62. data/lib/shopify_app/test_helpers/all.rb +1 -0
  63. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +16 -0
  64. data/lib/shopify_app/utils.rb +6 -5
  65. data/lib/shopify_app/version.rb +2 -1
  66. data/package-lock.json +1231 -1210
  67. data/package.json +1 -1
  68. data/shopify_app.gemspec +12 -8
  69. data/yarn.lock +3 -3
  70. metadata +32 -11
  71. data/lib/shopify_app/session/storage_strategies/shop_storage_strategy.rb +0 -23
  72. data/lib/shopify_app/session/storage_strategies/user_storage_strategy.rb +0 -24
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  class User < ActiveRecord::Base
2
- include ShopifyApp::SessionStorage
3
+ include ShopifyApp::UserSessionStorage
3
4
 
4
5
  def api_version
5
6
  ShopifyApp.configuration.api_version
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators/base'
2
3
  require 'rails/generators/active_record'
3
4
 
@@ -8,19 +9,19 @@ module ShopifyApp
8
9
  source_root File.expand_path('../templates', __FILE__)
9
10
 
10
11
  def create_user_model
11
- copy_file 'user.rb', 'app/models/user.rb'
12
+ copy_file('user.rb', 'app/models/user.rb')
12
13
  end
13
14
 
14
15
  def create_user_migration
15
- migration_template 'db/migrate/create_users.erb', 'db/migrate/create_users.rb'
16
+ migration_template('db/migrate/create_users.erb', 'db/migrate/create_users.rb')
16
17
  end
17
18
 
18
19
  def update_shopify_app_initializer
19
- gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemorySessionStore', 'User'
20
+ gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryUserSessionStore', 'User')
20
21
  end
21
22
 
22
23
  def create_user_fixtures
23
- copy_file 'users.yml', 'test/fixtures/users.yml'
24
+ copy_file('users.yml', 'test/fixtures/users.yml')
24
25
  end
25
26
 
26
27
  private
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators/base'
2
3
 
3
4
  module ShopifyApp
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'shopify_app/version'
2
3
 
3
4
  # deps
@@ -44,9 +45,13 @@ module ShopifyApp
44
45
  require 'shopify_app/middleware/same_site_cookie_middleware'
45
46
 
46
47
  # session
47
- require 'shopify_app/session/storage_strategies/shop_storage_strategy'
48
- require 'shopify_app/session/storage_strategies/user_storage_strategy'
49
- require 'shopify_app/session/session_storage'
50
- require 'shopify_app/session/session_repository'
51
48
  require 'shopify_app/session/in_memory_session_store'
49
+ require 'shopify_app/session/in_memory_shop_session_store'
50
+ require 'shopify_app/session/in_memory_user_session_store'
51
+ require 'shopify_app/session/jwt'
52
+ require 'shopify_app/session/null_user_session_store'
53
+ require 'shopify_app/session/session_repository'
54
+ require 'shopify_app/session/session_storage'
55
+ require 'shopify_app/session/shop_session_storage'
56
+ require 'shopify_app/session/user_session_storage'
52
57
  end
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class Configuration
3
-
4
4
  # Shopify App settings. These values should match the configuration
5
5
  # for the app in your Shopify Partners page. Change your settings in
6
6
  # `config/initializers/shopify_app.rb`
@@ -14,14 +14,11 @@ module ShopifyApp
14
14
  attr_accessor :webhooks
15
15
  attr_accessor :scripttags
16
16
  attr_accessor :after_authenticate_job
17
- attr_reader :session_repository
18
- attr_accessor :per_user_tokens
19
- alias_method :per_user_tokens?, :per_user_tokens
20
17
  attr_accessor :api_version
21
18
 
22
19
  # customise urls
23
20
  attr_accessor :root_url
24
- attr_accessor :login_url
21
+ attr_writer :login_url
25
22
 
26
23
  # customise ActiveJob queue names
27
24
  attr_accessor :scripttags_manager_queue_name
@@ -37,14 +34,16 @@ module ShopifyApp
37
34
  attr_accessor :webhook_jobs_namespace
38
35
 
39
36
  # allow enabling of same site none on cookies
40
- attr_accessor :enable_same_site_none
37
+ attr_writer :enable_same_site_none
38
+
39
+ # allow enabling jwt headers for authentication
40
+ attr_accessor :allow_jwt_authentication
41
41
 
42
42
  def initialize
43
43
  @root_url = '/'
44
44
  @myshopify_domain = 'myshopify.com'
45
45
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
46
46
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
47
- @per_user_tokens = false
48
47
  @disable_webpacker = ENV['SHOPIFY_APP_DISABLE_WEBPACKER'].present?
49
48
  end
50
49
 
@@ -52,9 +51,20 @@ module ShopifyApp
52
51
  @login_url || File.join(@root_url, 'login')
53
52
  end
54
53
 
55
- def session_repository=(klass)
56
- @session_repository = klass
57
- ShopifyApp::SessionRepository.storage = klass
54
+ def user_session_repository=(klass)
55
+ ShopifyApp::SessionRepository.user_storage = klass
56
+ end
57
+
58
+ def user_session_repository
59
+ ShopifyApp::SessionRepository.user_storage
60
+ end
61
+
62
+ def shop_session_repository=(klass)
63
+ ShopifyApp::SessionRepository.shop_storage = klass
64
+ end
65
+
66
+ def shop_session_repository
67
+ ShopifyApp::SessionRepository.shop_storage
58
68
  end
59
69
 
60
70
  def has_webhooks?
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module AppProxyVerification
3
4
  extend ActiveSupport::Concern
@@ -8,7 +9,7 @@ module ShopifyApp
8
9
  end
9
10
 
10
11
  def verify_proxy_request
11
- return head :forbidden unless query_string_valid?(request.query_string)
12
+ return head(:forbidden) unless query_string_valid?(request.query_string)
12
13
  end
13
14
 
14
15
  private
@@ -26,7 +27,7 @@ module ShopifyApp
26
27
  end
27
28
 
28
29
  def calculated_signature(query_hash_without_signature)
29
- sorted_params = query_hash_without_signature.collect{|k,v| "#{k}=#{Array(v).join(',')}"}.sort.join
30
+ sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
30
31
 
31
32
  OpenSSL::HMAC.hexdigest(
32
33
  OpenSSL::Digest.new('sha256'),
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module EmbeddedApp
3
4
  extend ActiveSupport::Concern
4
5
 
5
6
  included do
6
7
  if ShopifyApp.configuration.embedded_app?
7
- after_action :set_esdk_headers
8
- layout 'embedded_app'
8
+ after_action(:set_esdk_headers)
9
+ layout('embedded_app')
9
10
  end
10
11
  end
11
12
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module Localization
3
4
  extend ActiveSupport::Concern
@@ -11,56 +11,94 @@ module ShopifyApp
11
11
 
12
12
  included do
13
13
  after_action :set_test_cookie
14
- rescue_from ActiveResource::UnauthorizedAccess, :with => :close_session
14
+ rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
15
15
  end
16
16
 
17
- def shopify_session
18
- return redirect_to_login unless shop_session
17
+ def activate_shopify_session
18
+ return redirect_to_login if current_shopify_session.blank?
19
19
  clear_top_level_oauth_cookie
20
20
 
21
21
  begin
22
- ShopifyAPI::Base.activate_session(shop_session)
22
+ ShopifyAPI::Base.activate_session(current_shopify_session)
23
23
  yield
24
24
  ensure
25
25
  ShopifyAPI::Base.clear_session
26
26
  end
27
27
  end
28
28
 
29
- def shop_session
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])
29
+ def current_shopify_session
30
+ @current_shopify_session ||= begin
31
+ user_session || shop_session
36
32
  end
37
33
  end
38
34
 
35
+ def user_session
36
+ user_session_by_jwt || user_session_by_cookie
37
+ end
38
+
39
+ def user_session_by_jwt
40
+ return unless ShopifyApp.configuration.allow_jwt_authentication
41
+ return unless jwt_shopify_user_id
42
+ ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(jwt_shopify_user_id)
43
+ end
44
+
45
+ def user_session_by_cookie
46
+ return unless session[:user_id].present?
47
+ ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
48
+ end
49
+
50
+ def shop_session
51
+ shop_session_by_jwt || shop_session_by_cookie
52
+ end
53
+
54
+ def shop_session_by_jwt
55
+ return unless ShopifyApp.configuration.allow_jwt_authentication
56
+ return unless jwt_shopify_domain
57
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(jwt_shopify_domain)
58
+ end
59
+
60
+ def shop_session_by_cookie
61
+ return unless session[:shop_id].present?
62
+ ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
63
+ end
64
+
39
65
  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
66
+ if session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
67
+ clear_session = session[:user_session] != params[:session] # current user is different from stored user
43
68
 
44
- if valid_session_data && sessions_do_not_match
45
- clear_session = true
46
- end
47
69
  end
48
70
 
49
- if shop_session && params[:shop] && params[:shop].is_a?(String) && (shop_session.domain != params[:shop])
71
+ if current_shopify_session &&
72
+ params[:shop] && params[:shop].is_a?(String) &&
73
+ (current_shopify_session.domain != params[:shop])
50
74
  clear_session = true
51
75
  end
52
76
 
53
77
  if clear_session
54
- clear_shop_session
78
+ clear_shopify_session
55
79
  redirect_to_login
56
80
  end
57
81
  end
58
82
 
59
83
  protected
60
84
 
85
+ def jwt_shopify_domain
86
+ return unless jwt
87
+ @jwt_shopify_domain ||= JWT.new(jwt).shopify_domain
88
+ end
89
+
90
+ def jwt_shopify_user_id
91
+ return unless jwt
92
+ @jwt_user_id ||= JWT.new(jwt).shopify_user_id
93
+ end
94
+
95
+ def jwt
96
+ @jwt ||= authenticate_with_http_token { |token| token }
97
+ end
98
+
61
99
  def redirect_to_login
62
100
  if request.xhr?
63
- head :unauthorized
101
+ head(:unauthorized)
64
102
  else
65
103
  if request.get?
66
104
  path = request.path
@@ -76,12 +114,13 @@ module ShopifyApp
76
114
  end
77
115
 
78
116
  def close_session
79
- clear_shop_session
117
+ clear_shopify_session
80
118
  redirect_to(login_url_with_optional_shop)
81
119
  end
82
120
 
83
- def clear_shop_session
84
- session[:shopify] = nil
121
+ def clear_shopify_session
122
+ session[:shop_id] = nil
123
+ session[:user_id] = nil
85
124
  session[:shopify_domain] = nil
86
125
  session[:shopify_user] = nil
87
126
  session[:user_session] = nil
@@ -123,9 +162,10 @@ module ShopifyApp
123
162
 
124
163
  def fullpage_redirect_to(url)
125
164
  if ShopifyApp.configuration.embedded_app?
126
- render 'shopify_app/shared/redirect', layout: false, locals: { url: url, current_shopify_domain: current_shopify_domain }
165
+ render('shopify_app/shared/redirect', layout: false,
166
+ locals: { url: url, current_shopify_domain: current_shopify_domain })
127
167
  else
128
- redirect_to url
168
+ redirect_to(url)
129
169
  end
130
170
  end
131
171
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module WebhookVerification
3
4
  extend ActiveSupport::Concern
@@ -11,7 +12,7 @@ module ShopifyApp
11
12
 
12
13
  def verify_request
13
14
  data = request.raw_post
14
- return head :unauthorized unless hmac_valid?(data)
15
+ return head(:unauthorized) unless hmac_valid?(data)
15
16
  end
16
17
 
17
18
  def hmac_valid?(data)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class Engine < Rails::Engine
3
4
  engine_name 'shopify_app'
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class ScripttagsManagerJob < ActiveJob::Base
3
-
4
4
  queue_as do
5
5
  ShopifyApp.configuration.scripttags_manager_queue_name
6
6
  end
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class WebhooksManagerJob < ActiveJob::Base
3
-
4
4
  queue_as do
5
5
  ShopifyApp.configuration.webhooks_manager_queue_name
6
6
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class ScripttagsManager
3
4
  class CreationFailed < StandardError; end
@@ -43,7 +44,7 @@ module ShopifyApp
43
44
  def destroy_scripttags
44
45
  scripttags = expanded_scripttags
45
46
  ShopifyAPI::ScriptTag.all.each do |tag|
46
- ShopifyAPI::ScriptTag.delete(tag.id) if is_required_scripttag?(scripttags, tag)
47
+ ShopifyAPI::ScriptTag.delete(tag.id) if required_scripttag?(scripttags, tag)
47
48
  end
48
49
 
49
50
  @current_scripttags = nil
@@ -55,8 +56,8 @@ module ShopifyApp
55
56
  self.class.build_src(required_scripttags, shop_domain)
56
57
  end
57
58
 
58
- def is_required_scripttag?(scripttags, tag)
59
- scripttags.map{ |w| w[:src] }.include? tag.src
59
+ def required_scripttag?(scripttags, tag)
60
+ scripttags.map { |w| w[:src] }.include?(tag.src)
60
61
  end
61
62
 
62
63
  def create_scripttag(attributes)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class WebhooksManager
3
4
  class CreationFailed < StandardError; end
@@ -31,7 +32,7 @@ module ShopifyApp
31
32
 
32
33
  def destroy_webhooks
33
34
  ShopifyAPI::Webhook.all.to_a.each do |webhook|
34
- ShopifyAPI::Webhook.delete(webhook.id) if is_required_webhook?(webhook)
35
+ ShopifyAPI::Webhook.delete(webhook.id) if required_webhook?(webhook)
35
36
  end
36
37
 
37
38
  @current_webhooks = nil
@@ -39,8 +40,8 @@ module ShopifyApp
39
40
 
40
41
  private
41
42
 
42
- def is_required_webhook?(webhook)
43
- required_webhooks.map{ |w| w[:address] }.include? webhook.address
43
+ def required_webhook?(webhook)
44
+ required_webhooks.map { |w| w[:address] }.include?(webhook.address)
44
45
  end
45
46
 
46
47
  def create_webhook(attributes)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class SameSiteCookieMiddleware
3
4
  COOKIE_SEPARATOR = "\n"
@@ -11,14 +12,15 @@ module ShopifyApp
11
12
  user_agent = env['HTTP_USER_AGENT']
12
13
 
13
14
  if headers && headers['Set-Cookie'] &&
14
- !SameSiteCookieMiddleware.same_site_none_incompatible?(user_agent) &&
15
- ShopifyApp.configuration.enable_same_site_none
15
+ BrowserSniffer.new(user_agent).same_site_none_compatible? &&
16
+ ShopifyApp.configuration.enable_same_site_none &&
17
+ Rack::Request.new(env).ssl?
16
18
 
17
19
  set_cookies = headers['Set-Cookie']
18
20
  .split(COOKIE_SEPARATOR)
19
21
  .compact
20
22
  .map do |cookie|
21
- cookie << '; Secure' if not cookie =~ /;\s*secure/i
23
+ cookie << '; Secure' unless cookie =~ /;\s*secure/i
22
24
  cookie << '; SameSite=None' unless cookie =~ /;\s*samesite=/i
23
25
  cookie
24
26
  end
@@ -28,40 +30,5 @@ module ShopifyApp
28
30
 
29
31
  [status, headers, body]
30
32
  end
31
-
32
- def self.same_site_none_incompatible?(user_agent)
33
- sniffer = BrowserSniffer.new(user_agent)
34
-
35
- webkit_same_site_bug?(sniffer) || drops_unrecognized_same_site_cookies?(sniffer)
36
- rescue
37
- true
38
- end
39
-
40
- def self.webkit_same_site_bug?(sniffer)
41
- (sniffer.os == :ios && sniffer.os_version.match(/^([0-9]|1[12])[\.\_]/)) ||
42
- (sniffer.os == :mac && sniffer.browser == :safari && sniffer.os_version.match(/^10[\.\_]14/))
43
- end
44
-
45
- def self.drops_unrecognized_same_site_cookies?(sniffer)
46
- (chromium_based?(sniffer) && sniffer.major_browser_version >= 51 && sniffer.major_browser_version <= 66) ||
47
- (uc_browser?(sniffer) && !uc_browser_version_at_least?(sniffer: sniffer, major: 12, minor: 13, build: 2))
48
- end
49
-
50
- def self.chromium_based?(sniffer)
51
- sniffer.browser_name.downcase.match(/chrom(e|ium)/)
52
- end
53
-
54
- def self.uc_browser?(sniffer)
55
- sniffer.user_agent.downcase.match(/uc\s?browser/)
56
- end
57
-
58
- def self.uc_browser_version_at_least?(sniffer:, major:, minor:, build:)
59
- digits = sniffer.browser_version.split('.').map(&:to_i)
60
- return false unless digits.count >= 3
61
-
62
- return digits[0] > major if digits[0] != major
63
- return digits[1] > minor if digits[1] != minor
64
- digits[2] >= build
65
- end
66
33
  end
67
34
  end