Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 89 additions & 30 deletions src/services/login-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ define(function (require, exports, module) {
require("./setup-login-service"); // this adds loginService to KernalModeTrust
require("./promotions");

const Metrics = require("utils/Metrics");
const LoginUtils = require("./login-utils");

const MS_IN_DAY = 10 * 24 * 60 * 60 * 1000;
const TEN_MINUTES = 10 * 60 * 1000;

const KernalModeTrust = window.KernalModeTrust;
if(!KernalModeTrust){
Expand All @@ -43,6 +47,25 @@ define(function (require, exports, module) {
// Cached entitlements data
let cachedEntitlements = null;

// Last recorded state for entitlements monitoring
let lastRecordedState = null;

// Debounced trigger for entitlements changed
let entitlementsChangedTimer = null;

function _debounceEntitlementsChanged() {
if (entitlementsChangedTimer) {
// already scheduled, skip
return;
}

entitlementsChangedTimer = setTimeout(() => {
LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED);
entitlementsChangedTimer = null;
}, 1000); // atmost 1 entitlement changed event will be triggered in a second
}


/**
* Get entitlements from API or cache
* Returns null if user is not logged in
Expand Down Expand Up @@ -96,7 +119,7 @@ define(function (require, exports, module) {

// Trigger event if entitlements changed
if (entitlementsChanged) {
LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED, result);
_debounceEntitlementsChanged();
}

return cachedEntitlements;
Expand All @@ -116,12 +139,43 @@ define(function (require, exports, module) {
function clearEntitlements() {
if (cachedEntitlements) {
cachedEntitlements = null;
_debounceEntitlementsChanged();
}
}

// Trigger event when entitlements are cleared
if (LoginService.trigger) {
LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED, null);

/**
* Start the 10-minute interval timer for monitoring entitlements
*/
function startEntitlementsMonitor() {
setInterval(async () => {
try {
const current = await getEffectiveEntitlements(false); // Get effective entitlements

// Check if we need to refresh
const expiredPlanName = LoginUtils.validTillExpired(current, lastRecordedState);
const hasChanged = LoginUtils.haveEntitlementsChanged(current, lastRecordedState);

if (expiredPlanName || hasChanged) {
console.log(`Entitlements monitor detected changes, Expired: ${expiredPlanName},` +
`changed: ${hasChanged} refreshing...`);
Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "entRefresh",
expiredPlanName ? "exp_"+expiredPlanName : "changed");
await getEffectiveEntitlements(true); // Force refresh
// if not logged in, the getEffectiveEntitlements will not trigger change even if some trial
// entitlements changed. so we trigger a change anyway here. The debounce will take care of
// multi fire and we are ok with multi fire 1 second apart.
_debounceEntitlementsChanged();
}

// Update last recorded state
lastRecordedState = current;
} catch (error) {
console.error('Entitlements monitor error:', error);
}
}
}, TEN_MINUTES);

console.log('Entitlements monitor started (10-minute interval)');
}

/**
Expand Down Expand Up @@ -197,7 +251,8 @@ define(function (require, exports, module) {
* @example
* // Listen for entitlements changes
* const LoginService = window.KernelModeTrust.loginService;
* LoginService.on(LoginService.EVENT_ENTITLEMENTS_CHANGED, (entitlements) => {
* LoginService.on(LoginService.EVENT_ENTITLEMENTS_CHANGED, async() => {
* const entitlements = await LoginService.getEffectiveEntitlements();
* console.log('Entitlements changed:', entitlements);
* // Update UI based on new entitlements
* });
Expand Down Expand Up @@ -229,38 +284,20 @@ define(function (require, exports, module) {
if (serverEntitlements.plan.paidSubscriber) {
// Already a paid subscriber, return as-is
return serverEntitlements;
} else {
// Enhance entitlements for trial user
return {
...serverEntitlements,
plan: {
...serverEntitlements.plan,
paidSubscriber: true,
name: brackets.config.main_pro_plan
},
isInProTrial: true,
trialDaysRemaining: trialDaysRemaining,
entitlements: {
...serverEntitlements.entitlements,
liveEdit: {
activated: true,
subscribeURL: brackets.config.purchase_url,
upgradeToPlan: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
}
}
};
}
} else {
// Non-logged-in user with trial - return synthetic entitlements
// Enhance entitlements for trial user
return {
...serverEntitlements,
plan: {
...serverEntitlements.plan,
paidSubscriber: true,
name: brackets.config.main_pro_plan
name: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
},
isInProTrial: true,
trialDaysRemaining: trialDaysRemaining,
entitlements: {
...serverEntitlements.entitlements,
liveEdit: {
activated: true,
subscribeURL: brackets.config.purchase_url,
Expand All @@ -270,11 +307,33 @@ define(function (require, exports, module) {
}
};
}

// Non-logged-in user with trial - return synthetic entitlements
return {
plan: {
paidSubscriber: true,
name: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
},
isInProTrial: true,
trialDaysRemaining: trialDaysRemaining,
entitlements: {
liveEdit: {
activated: true,
subscribeURL: brackets.config.purchase_url,
upgradeToPlan: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
}
}
};
}

// Add functions to secure exports
LoginService.getEntitlements = getEntitlements;
LoginService.getEffectiveEntitlements = getEffectiveEntitlements;
LoginService.clearEntitlements = clearEntitlements;
LoginService.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED;

// Start the entitlements monitor timer
startEntitlementsMonitor();
});
135 changes: 135 additions & 0 deletions src/services/login-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/**
* Login Service Utilities
*
* This module contains utility functions for login service operations,
* including entitlements expiration checking and change detection.
*/

define(function (require, exports, module) {

/**
* Check if any validTill time has expired
*
* @param {Object|null} entitlements - Current entitlements object
* @param {Object|null} lastRecordedEntitlement - Previously recorded entitlements
* @returns {string|null} - Name of expired plan/entitlement or null if none expired
*/
function validTillExpired(entitlements, lastRecordedEntitlement) {
if (!entitlements) {
return null;
}

const now = Date.now();

function isNewlyExpired(validTill, lastValidTill) {
return (
validTill &&
validTill < now && // expired now
(!lastValidTill || lastValidTill >= now) // but wasn't expired before
);
}

// Check plan validTill
if (entitlements.plan) {
const validTill = entitlements.plan.validTill;
const lastValidTill = (lastRecordedEntitlement && lastRecordedEntitlement.plan)
? lastRecordedEntitlement.plan.validTill
: null;

if (isNewlyExpired(validTill, lastValidTill)) {
return entitlements.plan.name || brackets.config.main_pro_plan;
}
}

// Check entitlements validTill
if (entitlements.entitlements) {
for (const key in entitlements.entitlements) {
const entitlement = entitlements.entitlements[key];
if (!entitlement) {
continue;
}

const validTill = entitlement.validTill;
const lastValidTill = (lastRecordedEntitlement &&
lastRecordedEntitlement.entitlements &&
lastRecordedEntitlement.entitlements[key])
? lastRecordedEntitlement.entitlements[key].validTill
: null;

if (isNewlyExpired(validTill, lastValidTill)) {
return key;
}
}
}

return null;
}

/**
* Check if entitlements have changed from last recorded state
*
* @param {Object|null} current - Current entitlements object
* @param {Object|null} last - Last recorded entitlements object
* @returns {boolean} - True if entitlements have changed, false otherwise
*/
function haveEntitlementsChanged(current, last) {
if (!last && !current) {
return false;
}
if ((!last && current) || (!current && last)) {
return true;
}
if ((!last.entitlements && current.entitlements) || (!current.entitlements && last.entitlements)) {
return true;
}

// Check paidSubscriber changes
const currentPaidSub = current.plan && current.plan.paidSubscriber;
const lastPaidSub = last.plan && last.plan.paidSubscriber;
if (currentPaidSub !== lastPaidSub) {
return true;
}

// Check plan name changes
const currentPlanName = current.plan && current.plan.name;
const lastPlanName = last.plan && last.plan.name;
if (currentPlanName !== lastPlanName) {
return true;
}

// Check entitlement activations
if (current.entitlements && last.entitlements) {
for (const key of Object.keys(current.entitlements)) {
const currentActivated = current.entitlements[key] && current.entitlements[key].activated;
const lastActivated = last.entitlements[key] && last.entitlements[key].activated;
if (currentActivated !== lastActivated) {
return true;
}
}
}

return false;
}

// Export functions
exports.validTillExpired = validTillExpired;
exports.haveEntitlementsChanged = haveEntitlementsChanged;
});
4 changes: 2 additions & 2 deletions src/services/promotions.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ define(function (require, exports, module) {

// Also trigger entitlements changed event since effective entitlements have changed
// This allows UI components to update based on the new trial status
const effectiveEntitlements = await LoginService.getEffectiveEntitlements();
LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED, effectiveEntitlements);
await LoginService.getEffectiveEntitlements();
LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED);
}

function _isAnyDialogsVisible() {
Expand Down
1 change: 1 addition & 0 deletions test/UnitTestSuite.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ define(function (require, exports, module) {
require("spec/spacing-auto-detect-integ-test");
require("spec/LocalizationUtils-test");
require("spec/ScrollTrackHandler-integ-test");
require("spec/login-utils-test");
// Integrated extension tests
require("spec/Extn-InAppNotifications-integ-test");
require("spec/Extn-RemoteFileAdapter-integ-test");
Expand Down
Loading
Loading