@@ -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' } file. ` +
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 } day(s) ago. ` +
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 } day(s) remaining. ` +
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 } day(s) ago. ` +
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 } day(s) remaining. ` +
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