Skip to content

Commit 05b8615

Browse files
Implement license expiration handling with a 1-month grace period for production environments and update related tests for clarity and coverage
1 parent 118b9f1 commit 05b8615

File tree

5 files changed

+244
-31
lines changed

5 files changed

+244
-31
lines changed

react_on_rails_pro/LICENSE_SETUP.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,27 @@ The license is validated at multiple points:
8484

8585
React on Rails Pro requires a valid license in **all environments**:
8686

87-
-**Development**: Requires license (use FREE license)
88-
-**Test**: Requires license (use FREE license)
89-
-**CI/CD**: Requires license (use FREE license)
90-
-**Production**: Requires license (use paid license)
87+
-**Development**: Requires license (use FREE license) - **Fails immediately on expiration**
88+
-**Test**: Requires license (use FREE license) - **Fails immediately on expiration**
89+
-**CI/CD**: Requires license (use FREE license) - **Fails immediately on expiration**
90+
-**Production**: Requires license (use paid license) - **1-month grace period after expiration**
9191

9292
Get your FREE evaluation license in 30 seconds - no credit card required!
9393

94+
### Production Grace Period
95+
96+
**Production environments only** receive a **1-month grace period** when a license expires:
97+
98+
- ⚠️ **During grace period**: Application continues to run but logs ERROR messages on every startup
99+
-**After grace period**: Application fails to start (same as dev/test)
100+
- 🔔 **Warning messages**: Include days remaining in grace period
101+
-**Development/Test**: No grace period - fails immediately (helps catch expiration early)
102+
103+
**Important**: The grace period is designed to give production deployments time to renew, but you should:
104+
1. Monitor your logs for license expiration warnings
105+
2. Renew licenses before they expire
106+
3. Test license renewal in development/staging first
107+
94108
## Team Setup
95109

96110
### For Development Teams
@@ -158,11 +172,20 @@ window.railsContext.rorPro
158172

159173
### Error: "License has expired"
160174

175+
**What happens:**
176+
- **Development/Test/CI**: Application fails to start immediately
177+
- **Production**: 1-month grace period with ERROR logs, then fails to start
178+
161179
**Solutions:**
162180
1. **Free License**: Get a new 3-month FREE license
163181
2. **Paid License**: Contact support to renew
164182
3. Visit: [https://shakacode.com/react-on-rails-pro](https://shakacode.com/react-on-rails-pro)
165183

184+
**If you see grace period warnings in production:**
185+
- You have time to renew, but don't wait!
186+
- The warning shows how many days remain
187+
- Plan your license renewal before the grace period ends
188+
166189
### Error: "License is missing required expiration field"
167190

168191
**Cause:** You may have an old license format

react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ def license_data
3030

3131
private
3232

33+
# Grace period: 1 month (in seconds)
34+
GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60
35+
3336
def validate_license
3437
license = load_and_decode_license
3538

@@ -41,12 +44,28 @@ def validate_license
4144
handle_invalid_license(@validation_error)
4245
end
4346

44-
# Check expiry
45-
if Time.now.to_i > license["exp"]
46-
@validation_error = "License has expired. " \
47+
# Check expiry with grace period for production
48+
current_time = Time.now.to_i
49+
exp_time = license["exp"]
50+
51+
if current_time > exp_time
52+
days_expired = ((current_time - exp_time) / (24 * 60 * 60)).to_i
53+
54+
@validation_error = "License has expired #{days_expired} day(s) ago. " \
4755
"Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro " \
4856
"or upgrade to a paid license for production use."
49-
handle_invalid_license(@validation_error)
57+
58+
# In production, allow a grace period of 1 month with error logging
59+
if production? && within_grace_period?(exp_time)
60+
grace_days_remaining = grace_days_remaining(exp_time)
61+
Rails.logger.error(
62+
"[React on Rails Pro] WARNING: #{@validation_error} " \
63+
"Grace period: #{grace_days_remaining} day(s) remaining. " \
64+
"Application will fail to start after grace period expires."
65+
)
66+
else
67+
handle_invalid_license(@validation_error)
68+
end
5069
end
5170

5271
# Log license type if present (for analytics)
@@ -64,6 +83,20 @@ def validate_license
6483
handle_invalid_license(@validation_error)
6584
end
6685

86+
def production?
87+
Rails.env.production?
88+
end
89+
90+
def within_grace_period?(exp_time)
91+
Time.now.to_i <= exp_time + GRACE_PERIOD_SECONDS
92+
end
93+
94+
def grace_days_remaining(exp_time)
95+
grace_end = exp_time + GRACE_PERIOD_SECONDS
96+
seconds_remaining = grace_end - Time.now.to_i
97+
(seconds_remaining / (24 * 60 * 60)).to_i
98+
end
99+
67100
def load_and_decode_license
68101
license_string = load_license_string
69102

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ let cachedValid: boolean | undefined;
2323
let cachedLicenseData: LicenseData | undefined;
2424
let cachedValidationError: string | undefined;
2525

26+
// Grace period: 1 month (in seconds)
27+
const GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60;
28+
2629
/**
2730
* Handles invalid license by logging error and exiting.
2831
* @private
@@ -34,6 +37,32 @@ function handleInvalidLicense(message: string): never {
3437
process.exit(1);
3538
}
3639

40+
/**
41+
* Checks if the current environment is production.
42+
* @private
43+
*/
44+
function isProduction(): boolean {
45+
return process.env.NODE_ENV === 'production';
46+
}
47+
48+
/**
49+
* Checks if the license is within the grace period.
50+
* @private
51+
*/
52+
function isWithinGracePeriod(expTime: number): boolean {
53+
return Date.now() / 1000 <= expTime + GRACE_PERIOD_SECONDS;
54+
}
55+
56+
/**
57+
* Calculates remaining grace period days.
58+
* @private
59+
*/
60+
function graceDaysRemaining(expTime: number): number {
61+
const graceEnd = expTime + GRACE_PERIOD_SECONDS;
62+
const secondsRemaining = graceEnd - Date.now() / 1000;
63+
return Math.floor(secondsRemaining / (24 * 60 * 60));
64+
}
65+
3766
/**
3867
* Logs license information for analytics.
3968
* @private
@@ -119,14 +148,30 @@ function performValidation(): boolean | never {
119148
handleInvalidLicense(cachedValidationError);
120149
}
121150

122-
// Check expiry
151+
// Check expiry with grace period for production
123152
// Date.now() returns milliseconds, but JWT exp is in Unix seconds, so divide by 1000
124-
if (Date.now() / 1000 > license.exp) {
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+
125159
cachedValidationError =
126-
'License has expired. ' +
160+
`License has expired ${daysExpired} day(s) ago. ` +
127161
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
128162
'or upgrade to a paid license for production use.';
129-
handleInvalidLicense(cachedValidationError);
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+
}
130175
}
131176

132177
// Log license type if present (for analytics)

react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,25 +104,74 @@ describe('LicenseValidator', () => {
104104
expect(module.validateLicense()).toBe(true);
105105
});
106106

107-
it('calls process.exit for expired license', () => {
108-
const expiredPayload = {
109-
sub: 'test@example.com',
110-
iat: Math.floor(Date.now() / 1000) - 7200,
111-
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
112-
};
107+
describe('expired license behavior', () => {
108+
it('calls process.exit for expired license in development/test', () => {
109+
const expiredPayload = {
110+
sub: 'test@example.com',
111+
iat: Math.floor(Date.now() / 1000) - 7200,
112+
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
113+
};
114+
115+
const expiredToken = jwt.sign(expiredPayload, testPrivateKey, { algorithm: 'RS256' });
116+
mockLicenseEnv(expiredToken);
117+
// Ensure NODE_ENV is not production
118+
delete process.env.NODE_ENV;
119+
120+
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
121+
122+
// Call validateLicense which should trigger process.exit
123+
module.validateLicense();
124+
125+
// Verify process.exit was called with code 1
126+
expect(mockProcessExit).toHaveBeenCalledWith(1);
127+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('License has expired'));
128+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('FREE evaluation license'));
129+
});
113130

114-
const expiredToken = jwt.sign(expiredPayload, testPrivateKey, { algorithm: 'RS256' });
115-
mockLicenseEnv(expiredToken);
131+
it('logs warning but does not exit in production within grace period', () => {
132+
// Expired 10 days ago (within 30-day grace period)
133+
const expiredWithinGrace = {
134+
sub: 'test@example.com',
135+
iat: Math.floor(Date.now() / 1000) - 15 * 24 * 60 * 60,
136+
exp: Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60,
137+
};
116138

117-
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
139+
const expiredToken = jwt.sign(expiredWithinGrace, testPrivateKey, { algorithm: 'RS256' });
140+
mockLicenseEnv(expiredToken);
141+
process.env.NODE_ENV = 'production';
118142

119-
// Call validateLicense which should trigger process.exit
120-
module.validateLicense();
143+
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
121144

122-
// Verify process.exit was called with code 1
123-
expect(mockProcessExit).toHaveBeenCalledWith(1);
124-
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('License has expired'));
125-
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('FREE evaluation license'));
145+
// Should not exit
146+
expect(() => module.validateLicense()).not.toThrow();
147+
expect(mockProcessExit).not.toHaveBeenCalled();
148+
149+
// Should log warning
150+
expect(mockConsoleError).toHaveBeenCalledWith(
151+
expect.stringMatching(/WARNING:.*License has expired.*Grace period:.*day\(s\) remaining/),
152+
);
153+
});
154+
155+
it('calls process.exit in production outside grace period', () => {
156+
// Expired 35 days ago (outside 30-day grace period)
157+
const expiredOutsideGrace = {
158+
sub: 'test@example.com',
159+
iat: Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60,
160+
exp: Math.floor(Date.now() / 1000) - 35 * 24 * 60 * 60,
161+
};
162+
163+
const expiredToken = jwt.sign(expiredOutsideGrace, testPrivateKey, { algorithm: 'RS256' });
164+
mockLicenseEnv(expiredToken);
165+
process.env.NODE_ENV = 'production';
166+
167+
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
168+
169+
module.validateLicense();
170+
171+
// Verify process.exit was called with code 1
172+
expect(mockProcessExit).toHaveBeenCalledWith(1);
173+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('License has expired'));
174+
});
126175
});
127176

128177
it('calls process.exit for license missing exp field', () => {

react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,75 @@
7373
ENV["REACT_ON_RAILS_PRO_LICENSE"] = expired_token
7474
end
7575

76-
it "raises error" do
77-
expect { described_class.validate! }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
76+
context "in development/test environment" do
77+
before do
78+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
79+
end
80+
81+
it "raises error immediately" do
82+
expect { described_class.validate! }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
83+
end
84+
85+
it "includes FREE license information in error message" do
86+
expect { described_class.validate! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
87+
end
7888
end
7989

80-
it "includes FREE license information in error message" do
81-
expect { described_class.validate! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
90+
context "in production environment" do
91+
before do
92+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
93+
end
94+
95+
context "within grace period (expired < 1 month ago)" do
96+
let(:expired_within_grace) do
97+
{
98+
sub: "test@example.com",
99+
iat: Time.now.to_i - (15 * 24 * 60 * 60), # Issued 15 days ago
100+
exp: Time.now.to_i - (10 * 24 * 60 * 60) # Expired 10 days ago (within 1 month grace)
101+
}
102+
end
103+
104+
before do
105+
token = JWT.encode(expired_within_grace, test_private_key, "RS256")
106+
ENV["REACT_ON_RAILS_PRO_LICENSE"] = token
107+
end
108+
109+
it "does not raise error" do
110+
expect { described_class.validate! }.not_to raise_error
111+
end
112+
113+
it "logs warning with grace period remaining" do
114+
expect(mock_logger).to receive(:error).with(/WARNING:.*License has expired.*Grace period:.*day\(s\) remaining/)
115+
described_class.validate!
116+
end
117+
118+
it "returns true" do
119+
expect(described_class.validate!).to be true
120+
end
121+
end
122+
123+
context "outside grace period (expired > 1 month ago)" do
124+
let(:expired_outside_grace) do
125+
{
126+
sub: "test@example.com",
127+
iat: Time.now.to_i - (60 * 24 * 60 * 60), # Issued 60 days ago
128+
exp: Time.now.to_i - (35 * 24 * 60 * 60) # Expired 35 days ago (outside 1 month grace)
129+
}
130+
end
131+
132+
before do
133+
token = JWT.encode(expired_outside_grace, test_private_key, "RS256")
134+
ENV["REACT_ON_RAILS_PRO_LICENSE"] = token
135+
end
136+
137+
it "raises error" do
138+
expect { described_class.validate! }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
139+
end
140+
141+
it "includes FREE license information in error message" do
142+
expect { described_class.validate! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
143+
end
144+
end
82145
end
83146
end
84147

0 commit comments

Comments
 (0)