@@ -18,14 +18,13 @@ interface LicenseData {
1818  [ key : string ] : unknown ; 
1919} 
2020
21- // Module-level state for caching 
22- let  cachedValid : boolean  |  undefined ; 
23- let  cachedLicenseData : LicenseData  |  undefined ; 
24- let  cachedValidationError : string  |  undefined ; 
25- 
2621// Grace period: 1 month (in seconds) 
2722const  GRACE_PERIOD_SECONDS  =  30  *  24  *  60  *  60 ; 
2823
24+ // Module-level state for caching 
25+ let  cachedLicenseData : LicenseData  |  undefined ; 
26+ let  cachedGraceDaysRemaining : number  |  undefined ; 
27+ 
2928/** 
3029 * Handles invalid license by logging error and exiting. 
3130 * @private  
@@ -38,29 +37,29 @@ function handleInvalidLicense(message: string): never {
3837} 
3938
4039/** 
41-  * Checks if the current environment is  production. 
40+  * Checks if running in  production environment . 
4241 * @private  
4342 */ 
4443function  isProduction ( ) : boolean  { 
4544  return  process . env . NODE_ENV  ===  'production' ; 
4645} 
4746
4847/** 
49-  * Checks if the license  is within the  grace period. 
48+  * Checks if current time  is within grace period after expiration . 
5049 * @private  
5150 */ 
5251function  isWithinGracePeriod ( expTime : number ) : boolean  { 
53-   return  Date . now ( )  /  1000  <=  expTime  +  GRACE_PERIOD_SECONDS ; 
52+   return  Math . floor ( Date . now ( )  /  1000 )  <=  expTime  +  GRACE_PERIOD_SECONDS ; 
5453} 
5554
5655/** 
5756 * Calculates remaining grace period days. 
5857 * @private  
5958 */ 
60- function  graceDaysRemaining ( expTime : number ) : number  { 
59+ function  calculateGraceDaysRemaining ( expTime : number ) : number  { 
6160  const  graceEnd  =  expTime  +  GRACE_PERIOD_SECONDS ; 
62-   const  secondsRemaining  =  graceEnd  -  Date . now ( )  /  1000 ; 
63-   return  Math . floor ( secondsRemaining  /  ( 24  *  60  *  60 ) ) ; 
61+   const  secondsRemaining  =  graceEnd  -  Math . floor ( Date . now ( )  /  1000 ) ; 
62+   return  secondsRemaining   <=   0  ?  0  :  Math . floor ( secondsRemaining  /  ( 24  *  60  *  60 ) ) ; 
6463} 
6564
6665/** 
@@ -83,30 +82,29 @@ function logLicenseInfo(license: LicenseData): void {
8382 * @private  
8483 */ 
8584// eslint-disable-next-line consistent-return 
86- function  loadLicenseString ( ) : string  |   never   { 
85+ function  loadLicenseString ( ) : string  { 
8786  // First try environment variable 
8887  const  envLicense  =  process . env . REACT_ON_RAILS_PRO_LICENSE ; 
8988  if  ( envLicense )  { 
9089    return  envLicense ; 
9190  } 
9291
9392  // Then try config file (relative to project root) 
94-   let  configPath ; 
9593  try  { 
96-     configPath  =  path . join ( process . cwd ( ) ,  'config' ,  'react_on_rails_pro_license.key' ) ; 
94+     const   configPath  =  path . join ( process . cwd ( ) ,  'config' ,  'react_on_rails_pro_license.key' ) ; 
9795    if  ( fs . existsSync ( configPath ) )  { 
9896      return  fs . readFileSync ( configPath ,  'utf8' ) . trim ( ) ; 
9997    } 
10098  }  catch  ( error )  { 
10199    console . error ( `[React on Rails Pro] Error reading license file: ${ ( error  as  Error ) . message }  ) ; 
102100  } 
103101
104-   cachedValidationError  = 
102+   const   errorMsg  = 
105103    'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable '  + 
106-     ` or create ${ configPath   ??   ' config/react_on_rails_pro_license.key' } ` + 
104+     ' or create config/react_on_rails_pro_license.key file. ' + 
107105    'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ; 
108106
109-   handleInvalidLicense ( cachedValidationError ) ; 
107+   handleInvalidLicense ( errorMsg ) ; 
110108} 
111109
112110/** 
@@ -126,119 +124,136 @@ function loadAndDecodeLicense(): LicenseData {
126124    ignoreExpiration : true , 
127125  } )  as  LicenseData ; 
128126
129-   cachedLicenseData  =  decoded ; 
130127  return  decoded ; 
131128} 
132129
133130/** 
134-  * Performs the actual license validation logic. 
131+  * Validates the license data and throws if invalid. 
132+  * Logs info/errors and handles grace period logic. 
133+  * 
134+  * @param  license - The decoded license data 
135+  * @returns  Grace days remaining if in grace period, undefined otherwise 
136+  * @throws  Never returns - exits process if license is invalid 
135137 * @private  
136138 */ 
137- // eslint-disable-next-line consistent-return 
138- function  performValidation ( ) : boolean  |  never  { 
139-   try  { 
140-     const  license  =  loadAndDecodeLicense ( ) ; 
139+ function  validateLicenseData ( license : LicenseData ) : number  |  undefined  { 
140+   // Check that exp field exists 
141+   if  ( ! license . exp )  { 
142+     const  error  = 
143+       'License is missing required expiration field. '  + 
144+       'Your license may be from an older version. '  + 
145+       'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ; 
146+     handleInvalidLicense ( error ) ; 
147+   } 
141148
142-     // Check that exp field exists 
143-     if  ( ! license . exp )  { 
144-       cachedValidationError  = 
145-         'License is missing required expiration field. '  + 
146-         'Your license may be from an older version. '  + 
147-         'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ; 
148-       handleInvalidLicense ( cachedValidationError ) ; 
149-     } 
149+   // Check expiry with grace period for production 
150+   const  currentTime  =  Math . floor ( Date . now ( )  /  1000 ) ; 
151+   const  expTime  =  license . exp ; 
152+   let  graceDays : number  |  undefined ; 
153+ 
154+   if  ( currentTime  >  expTime )  { 
155+     const  daysExpired  =  Math . floor ( ( currentTime  -  expTime )  /  ( 24  *  60  *  60 ) ) ; 
150156
151-     // Check expiry with grace period for production 
152-     // Date.now() returns milliseconds, but JWT exp is in Unix seconds, so divide by 1000 
153-     const  currentTime  =  Date . now ( )  /  1000 ; 
154-     const  expTime  =  license . exp ; 
155- 
156-     if  ( currentTime  >  expTime )  { 
157-       const  daysExpired  =  Math . floor ( ( currentTime  -  expTime )  /  ( 24  *  60  *  60 ) ) ; 
158- 
159-       cachedValidationError  = 
160-         `License has expired ${ daysExpired }   + 
161-         'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro '  + 
162-         'or upgrade to a paid license for production use.' ; 
163- 
164-       // In production, allow a grace period of 1 month with error logging 
165-       if  ( isProduction ( )  &&  isWithinGracePeriod ( expTime ) )  { 
166-         const  graceDays  =  graceDaysRemaining ( expTime ) ; 
167-         console . error ( 
168-           `[React on Rails Pro] WARNING: ${ cachedValidationError }   + 
169-             `Grace period: ${ graceDays }   + 
170-             'Application will fail to start after grace period expires.' , 
171-         ) ; 
172-       }  else  { 
173-         handleInvalidLicense ( cachedValidationError ) ; 
174-       } 
157+     const  error  = 
158+       `License has expired ${ daysExpired }   + 
159+       'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro '  + 
160+       'or upgrade to a paid license for production use.' ; 
161+ 
162+     // In production, allow a grace period of 1 month with error logging 
163+     if  ( isProduction ( )  &&  isWithinGracePeriod ( expTime ) )  { 
164+       // Calculate grace days once here 
165+       graceDays  =  calculateGraceDaysRemaining ( expTime ) ; 
166+       console . error ( 
167+         `[React on Rails Pro] WARNING: ${ error }   + 
168+           `Grace period: ${ graceDays }   + 
169+           'Application will fail to start after grace period expires.' , 
170+       ) ; 
171+     }  else  { 
172+       handleInvalidLicense ( error ) ; 
175173    } 
174+   } 
175+ 
176+   // Log license type if present (for analytics) 
177+   logLicenseInfo ( license ) ; 
176178
177-     // Log license type if present (for analytics) 
178-     logLicenseInfo ( license ) ; 
179+   // Return grace days (undefined if not in grace period) 
180+   return  graceDays ; 
181+ } 
179182
180-     return  true ; 
183+ /** 
184+  * Validates the license and returns the license data. 
185+  * Caches the result after first validation. 
186+  * 
187+  * @returns  The validated license data 
188+  * @throws  Exits process if license is invalid 
189+  */ 
190+ // eslint-disable-next-line consistent-return 
191+ export  function  getValidatedLicenseData ( ) : LicenseData  { 
192+   if  ( cachedLicenseData  !==  undefined )  { 
193+     return  cachedLicenseData ; 
194+   } 
195+ 
196+   try  { 
197+     // Load and decode license (but don't cache yet) 
198+     const  licenseData  =  loadAndDecodeLicense ( ) ; 
199+ 
200+     // Validate the license (raises if invalid, returns grace_days) 
201+     const  graceDays  =  validateLicenseData ( licenseData ) ; 
202+ 
203+     // Validation passed - now cache both data and grace days 
204+     cachedLicenseData  =  licenseData ; 
205+     cachedGraceDaysRemaining  =  graceDays ; 
206+ 
207+     return  cachedLicenseData ; 
181208  }  catch  ( error : unknown )  { 
182209    if  ( error  instanceof  Error  &&  error . name  ===  'JsonWebTokenError' )  { 
183-       cachedValidationError  = 
210+       const   errorMsg  = 
184211        `Invalid license signature: ${ error . message }   + 
185212        'Your license file may be corrupted. '  + 
186213        'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ; 
214+       handleInvalidLicense ( errorMsg ) ; 
187215    }  else  if  ( error  instanceof  Error )  { 
188-       cachedValidationError  = 
216+       const   errorMsg  = 
189217        `License validation error: ${ error . message }   + 
190218        'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ; 
219+       handleInvalidLicense ( errorMsg ) ; 
191220    }  else  { 
192-       cachedValidationError  = 
221+       const   errorMsg  = 
193222        'License validation error: Unknown error. '  + 
194223        'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ; 
224+       handleInvalidLicense ( errorMsg ) ; 
195225    } 
196-     handleInvalidLicense ( cachedValidationError ) ; 
197226  } 
198227} 
199228
200229/** 
201-  * Validates the license and exits the process if invalid. 
202-  * Caches the result after first validation. 
230+  * Checks if the current license is an evaluation/free license. 
203231 * 
204-  * @returns  true if license is valid 
205-  * @throws  Exits process if license is invalid 
232+  * @returns  true if plan is not "paid" 
206233 */ 
207- export  function  validateLicense ( ) : boolean  { 
208-   if  ( cachedValid  !==  undefined )  { 
209-     return  cachedValid ; 
210-   } 
211- 
212-   cachedValid  =  performValidation ( ) ; 
213-   return  cachedValid ; 
234+ export  function  isEvaluation ( ) : boolean  { 
235+   const  data  =  getValidatedLicenseData ( ) ; 
236+   const  plan  =  String ( data . plan  ||  '' ) ; 
237+   return  plan  !==  'paid'  &&  ! plan . startsWith ( 'paid_' ) ; 
214238} 
215239
216240/** 
217-  * Gets the decoded  license data . 
241+  * Returns remaining grace period days if  license is expired but in grace period . 
218242 * 
219-  * @returns  Decoded license data  or undefined if no license  
243+  * @returns  Number of days remaining,  or undefined if not in grace period  
220244 */ 
221- export  function  getLicenseData ( ) : LicenseData  |  undefined  { 
222-   if  ( ! cachedLicenseData )  { 
223-     loadAndDecodeLicense ( ) ; 
224-   } 
225-   return  cachedLicenseData ; 
226- } 
245+ export  function  getGraceDaysRemaining ( ) : number  |  undefined  { 
246+   // Ensure license is validated and cached 
247+   getValidatedLicenseData ( ) ; 
227248
228- /** 
229-  * Gets the validation error message if validation failed. 
230-  * 
231-  * @returns  Error message or undefined 
232-  */ 
233- export  function  getValidationError ( ) : string  |  undefined  { 
234-   return  cachedValidationError ; 
249+   // Return cached grace days (undefined if not in grace period) 
250+   return  cachedGraceDaysRemaining ; 
235251} 
236252
237253/** 
238254 * Resets all cached validation state (primarily for testing). 
239255 */ 
240256export  function  reset ( ) : void { 
241-   cachedValid  =  undefined ; 
242257  cachedLicenseData  =  undefined ; 
243-   cachedValidationError  =  undefined ; 
258+   cachedGraceDaysRemaining  =  undefined ; 
244259} 
0 commit comments