55module ReactOnRailsPro
66 class LicenseValidator
77 class << self
8- # Validates the license and raises an exception if invalid.
9- # Caches the result after first validation.
10- #
11- # @return [Boolean] true if license is valid
8+ # Validates the license and returns the license data
9+ # Caches the result after first validation
10+ # @return [Hash] The license data
1211 # @raise [ReactOnRailsPro::Error] if license is invalid
13- def validate!
14- return @validate if defined? ( @validate )
15-
16- @validate = validate_license
12+ def validated_license_data!
13+ return @license_data if defined? ( @license_data )
14+
15+ begin
16+ # Load and decode license (but don't cache yet)
17+ license_data = load_and_decode_license
18+
19+ # Validate the license (raises if invalid, returns grace_days)
20+ grace_days = validate_license_data ( license_data )
21+
22+ # Validation passed - now cache both data and grace days
23+ @license_data = license_data
24+ @grace_days_remaining = grace_days
25+
26+ @license_data
27+ rescue JWT ::DecodeError => e
28+ error = "Invalid license signature: #{ e . message } . " \
29+ "Your license file may be corrupted. " \
30+ "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
31+ handle_invalid_license ( error )
32+ rescue StandardError => e
33+ error = "License validation error: #{ e . message } . " \
34+ "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
35+ handle_invalid_license ( error )
36+ end
1737 end
1838
1939 def reset!
20- remove_instance_variable ( :@validate ) if defined? ( @validate )
2140 remove_instance_variable ( :@license_data ) if defined? ( @license_data )
22- remove_instance_variable ( :@validation_error ) if defined? ( @validation_error )
41+ remove_instance_variable ( :@grace_days_remaining ) if defined? ( @grace_days_remaining )
2342 end
2443
25- def license_data
26- @license_data ||= load_and_decode_license
44+ # Checks if the current license is an evaluation/free license
45+ # @return [Boolean] true if plan is not "paid"
46+ def evaluation?
47+ data = validated_license_data!
48+ plan = data [ "plan" ]
49+ plan != "paid"
2750 end
2851
29- attr_reader :validation_error
52+ # Returns remaining grace period days if license is expired but in grace period
53+ # @return [Integer, nil] Number of days remaining, or nil if not in grace period
54+ def grace_days_remaining
55+ # Ensure license is validated and cached
56+ validated_license_data!
57+
58+ # Return cached grace days (nil if not in grace period)
59+ @grace_days_remaining
60+ end
3061
3162 private
3263
3364 # Grace period: 1 month (in seconds)
3465 GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60
3566
36- def validate_license
37- license = load_and_decode_license
38-
67+ # Validates the license data and raises if invalid
68+ # Logs info/errors and handles grace period logic
69+ # @param license [Hash] The decoded license data
70+ # @return [Integer, nil] Grace days remaining if in grace period, nil otherwise
71+ # @raise [ReactOnRailsPro::Error] if license is invalid
72+ def validate_license_data ( license )
3973 # Check that exp field exists
4074 unless license [ "exp" ]
41- @validation_error = "License is missing required expiration field. " \
42- "Your license may be from an older version. " \
43- "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
44- handle_invalid_license ( @validation_error )
75+ error = "License is missing required expiration field. " \
76+ "Your license may be from an older version. " \
77+ "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
78+ handle_invalid_license ( error )
4579 end
4680
4781 # Check expiry with grace period for production
4882 current_time = Time . now . to_i
4983 exp_time = license [ "exp" ]
84+ grace_days = nil
5085
5186 if current_time > exp_time
5287 days_expired = ( ( current_time - exp_time ) / ( 24 * 60 * 60 ) ) . to_i
5388
54- @validation_error = "License has expired #{ days_expired } day(s) ago. " \
55- "Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro " \
56- "or upgrade to a paid license for production use."
89+ error = "License has expired #{ days_expired } day(s) ago. " \
90+ "Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro " \
91+ "or upgrade to a paid license for production use."
5792
5893 # In production, allow a grace period of 1 month with error logging
5994 if production? && within_grace_period? ( exp_time )
60- grace_days_remaining = grace_days_remaining ( exp_time )
95+ # Calculate grace days once here
96+ grace_days = calculate_grace_days_remaining ( exp_time )
6197 Rails . logger . error (
62- "[React on Rails Pro] WARNING: #{ @validation_error } " \
63- "Grace period: #{ grace_days_remaining } day(s) remaining. " \
98+ "[React on Rails Pro] WARNING: #{ error } " \
99+ "Grace period: #{ grace_days } day(s) remaining. " \
64100 "Application will fail to start after grace period expires."
65101 )
66102 else
67- handle_invalid_license ( @validation_error )
103+ handle_invalid_license ( error )
68104 end
69105 end
70106
71107 # Log license type if present (for analytics)
72108 log_license_info ( license )
73109
74- true
75- rescue JWT ::DecodeError => e
76- @validation_error = "Invalid license signature: #{ e . message } . " \
77- "Your license file may be corrupted. " \
78- "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
79- handle_invalid_license ( @validation_error )
80- rescue StandardError => e
81- @validation_error = "License validation error: #{ e . message } . " \
82- "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
83- handle_invalid_license ( @validation_error )
110+ # Return grace days (nil if not in grace period)
111+ grace_days
84112 end
85113
86114 def production?
@@ -91,9 +119,14 @@ def within_grace_period?(exp_time)
91119 Time . now . to_i <= exp_time + GRACE_PERIOD_SECONDS
92120 end
93121
94- def grace_days_remaining ( exp_time )
122+ # Calculates remaining grace period days
123+ # @param exp_time [Integer] Expiration timestamp
124+ # @return [Integer] Days remaining (0 or more)
125+ def calculate_grace_days_remaining ( exp_time )
95126 grace_end = exp_time + GRACE_PERIOD_SECONDS
96127 seconds_remaining = grace_end - Time . now . to_i
128+ return 0 if seconds_remaining <= 0
129+
97130 ( seconds_remaining / ( 24 * 60 * 60 ) ) . to_i
98131 end
99132
@@ -114,7 +147,7 @@ def load_and_decode_license
114147 algorithm : "RS256" ,
115148 # Disable automatic expiration verification so we can handle it manually with custom logic
116149 verify_expiration : false
117- # JWT.decode returns an array [data, header]; we use `.first` to get the data (payload).
150+ # JWT.decode returns an array [data, header]; we use `.first` to get the data (payload).
118151 ) . first
119152 end
120153
0 commit comments