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
3 changes: 2 additions & 1 deletion src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1681,5 +1681,6 @@ define({
"PROMO_GET_APP_UPSELL_BUTTON": "Get {0}",
"PROMO_PRO_ENDED_TITLE": "Your {0} Trial has ended",
"PROMO_PRO_TRIAL_DAYS_LEFT": "Phoenix Pro Trial ({0} days left)",
"GET_PHOENIX_PRO": "Get Phoenix Pro"
"GET_PHOENIX_PRO": "Get Phoenix Pro",
"USER_FREE_PLAN_NAME": "Free Plan"
});
60 changes: 49 additions & 11 deletions src/services/login-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@
define(function (require, exports, module) {
require("./setup-login-service"); // this adds loginService to KernalModeTrust
require("./promotions");
require("./login-utils");

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

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

// the fallback salt is always a constant as this will only fail in rare circumstatnces and it needs to
// be exactly same across versions of the app. Changing this will not affect the large majority of users and
Expand All @@ -48,6 +50,7 @@ define(function (require, exports, module) {

// save a copy of window.fetch so that extensions wont tamper with it.
let fetchFn = window.fetch;
let dateNowFn = Date.now;

const KernalModeTrust = window.KernalModeTrust;
if(!KernalModeTrust){
Expand Down Expand Up @@ -351,8 +354,8 @@ define(function (require, exports, module) {
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);
const expiredPlanName = KernalModeTrust.LoginUtils.validTillExpired(current, lastRecordedState);
const hasChanged = KernalModeTrust.LoginUtils.haveEntitlementsChanged(current, lastRecordedState);

if (expiredPlanName || hasChanged) {
console.log(`Entitlements monitor detected changes, Expired: ${expiredPlanName},` +
Expand All @@ -376,6 +379,38 @@ define(function (require, exports, module) {
console.log('Entitlements monitor started (10-minute interval)');
}

function _validateAndFilterEntitlements(entitlements) {
if (!entitlements) {
return;
}

const currentDate = dateNowFn();

if(entitlements.plan && (!entitlements.plan.validTill || currentDate > entitlements.plan.validTill)) {
entitlements.plan = {
...entitlements.plan,
paidSubscriber: false,
name: Strings.USER_FREE_PLAN_NAME,
validTill: currentDate + (FREE_PLAN_VALIDITY_DAYS * MS_IN_DAY)
};
}

const featureEntitlements = entitlements.entitlements;
if (!featureEntitlements) {
return;
}

for(const featureName in featureEntitlements) {
const feature = featureEntitlements[featureName];
if(feature && (!feature.validTill || currentDate > feature.validTill)) {
feature.activated = false;
feature.upgradeToPlan = feature.upgradeToPlan || brackets.config.main_pro_plan;
feature.subscribeURL = feature.subscribeURL || brackets.config.purchase_url;
feature.validTill = feature.validTill || (currentDate - MS_IN_DAY);
}
}
}

/**
* Get effective entitlements for determining feature availability throughout the app.
* This is the primary API that should be used across Phoenix to check entitlements and enable/disable features.
Expand Down Expand Up @@ -467,6 +502,7 @@ define(function (require, exports, module) {
async function getEffectiveEntitlements(forceRefresh = false) {
// Get raw server entitlements
const serverEntitlements = await getEntitlements(forceRefresh);
_validateAndFilterEntitlements(serverEntitlements); // will prune invalid entitlements

// Get trial days remaining
const trialDaysRemaining = await LoginService.getProTrialDaysRemaining();
Expand All @@ -481,19 +517,17 @@ define(function (require, exports, module) {
// Logged-in user with trial
if (serverEntitlements.plan.paidSubscriber) {
// Already a paid subscriber, return as-is
// todo we need to check and filter valid till for each fields that we are interested in.
return serverEntitlements;
}
// Enhance entitlements for trial user
// todo we need to prune and filter serverEntitlements valid till for each fields that we are interested in.
// ie if any entitlement has valid till expired, we need to deactivate that entitlement
return {
...serverEntitlements,
plan: {
...serverEntitlements.plan,
paidSubscriber: true,
name: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
validTill: dateNowFn() + trialDaysRemaining * MS_IN_DAY
},
isInProTrial: true,
trialDaysRemaining: trialDaysRemaining,
Expand All @@ -503,7 +537,7 @@ define(function (require, exports, module) {
activated: true,
subscribeURL: brackets.config.purchase_url,
upgradeToPlan: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
validTill: dateNowFn() + trialDaysRemaining * MS_IN_DAY
}
}
};
Expand All @@ -514,7 +548,7 @@ define(function (require, exports, module) {
plan: {
paidSubscriber: true,
name: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
validTill: dateNowFn() + trialDaysRemaining * MS_IN_DAY
},
isInProTrial: true,
trialDaysRemaining: trialDaysRemaining,
Expand All @@ -523,7 +557,7 @@ define(function (require, exports, module) {
activated: true,
subscribeURL: brackets.config.purchase_url,
upgradeToPlan: brackets.config.main_pro_plan,
validTill: Date.now() + trialDaysRemaining * MS_IN_DAY
validTill: dateNowFn() + trialDaysRemaining * MS_IN_DAY
}
}
};
Expand All @@ -542,7 +576,11 @@ define(function (require, exports, module) {
LoginService,
setFetchFn: function _setFetchFn(fn) {
fetchFn = fn;
}
},
setDateNowFn: function _setDdateNowFn(fn) {
dateNowFn = fn;
},
_validateAndFilterEntitlements: _validateAndFilterEntitlements
};
}

Expand Down
24 changes: 18 additions & 6 deletions src/services/login-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@

define(function (require, exports, module) {

const KernalModeTrust = window.KernalModeTrust;
if(!KernalModeTrust){
// integrated extensions will have access to kernal mode, but not external extensions
throw new Error("Login utils should have access to KernalModeTrust. Cannot boot without trust ring");
}

/**
* 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
Expand Down Expand Up @@ -85,7 +91,7 @@ define(function (require, exports, module) {

/**
* 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
Expand Down Expand Up @@ -129,7 +135,13 @@ define(function (require, exports, module) {
return false;
}

// Export functions
exports.validTillExpired = validTillExpired;
exports.haveEntitlementsChanged = haveEntitlementsChanged;
});
KernalModeTrust.LoginUtils = {
validTillExpired,
haveEntitlementsChanged
};
// Test only Export functions
if(Phoenix.isTestWindow) {
exports.validTillExpired = validTillExpired;
exports.haveEntitlementsChanged = haveEntitlementsChanged;
}
});
2 changes: 1 addition & 1 deletion src/services/profile-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ define(function (require, exports, module) {
initials: profileData.profileIcon.initials,
avatarColor: profileData.profileIcon.color,
planClass: "user-plan-free",
planName: "Free Plan",
planName: Strings.USER_FREE_PLAN_NAME,
titleText: "Ai Quota Used",
usageText: "100 / 200 credits",
usedPercent: 0,
Expand Down
10 changes: 7 additions & 3 deletions test/spec/login-browser-integ-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ define(function (require, exports, module) {
// Note: We can't easily reset login state, so tests should handle this
});

function setupProUserMock(hasActiveSubscription = true) {
function setupProUserMock(hasActiveSubscription = true, expiredEntitlements = false) {
let userSignedOut = false;

// Set fetch mock on both browser and service exports
Expand Down Expand Up @@ -174,15 +174,19 @@ define(function (require, exports, module) {
};

if (hasActiveSubscription) {
const validTill = expiredEntitlements ?
Date.now() - 86400000 : // expired yesterday
Date.now() + 30 * 24 * 60 * 60 * 1000; // valid for 30 days

entitlementsResponse.plan = {
paidSubscriber: true,
name: "Phoenix Pro",
validTill: Date.now() + 30 * 24 * 60 * 60 * 1000
validTill: validTill
};
entitlementsResponse.entitlements = {
liveEdit: {
activated: true,
validTill: Date.now() + 30 * 24 * 60 * 60 * 1000
validTill: validTill
}
};
} else {
Expand Down
10 changes: 7 additions & 3 deletions test/spec/login-desktop-integ-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ define(function (require, exports, module) {
// Note: We can't easily reset login state, so tests should handle this
});

function setupProUserMock(hasActiveSubscription = true) {
function setupProUserMock(hasActiveSubscription = true, expiredEntitlements = false) {
let userSignedOut = false;

// Set fetch mock on desktop exports
Expand Down Expand Up @@ -196,15 +196,19 @@ define(function (require, exports, module) {
};

if (hasActiveSubscription) {
const validTill = expiredEntitlements ?
Date.now() - 86400000 : // expired yesterday
Date.now() + 30 * 24 * 60 * 60 * 1000; // valid for 30 days

entitlementsResponse.plan = {
paidSubscriber: true,
name: "Phoenix Pro",
validTill: Date.now() + 30 * 24 * 60 * 60 * 1000
validTill: validTill
};
entitlementsResponse.entitlements = {
liveEdit: {
activated: true,
validTill: Date.now() + 30 * 24 * 60 * 60 * 1000
validTill: validTill
}
};
} else {
Expand Down
84 changes: 84 additions & 0 deletions test/spec/login-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,90 @@ define(function (require, exports, module) {
// Close popup
$profileButton.trigger('click');
});

it("should show free user popup when entitlements are expired (no trial)", async function () {
console.log("llgT: Starting expired entitlements without trial test");

// Setup: Expired pro subscription + no trial
setupProUserMock(true, true);
await cleanupTrialState(); // Ensure no trial is active

// Verify initial state (no pro branding due to expired entitlements)
await verifyProBranding(false, "no pro branding initially due to expired entitlements");

// Perform login
await performFullLoginFlow();

// Verify pro branding remains false after login (expired entitlements filtered to free)
await verifyProBranding(false, "no pro branding after login with expired entitlements");

// Check profile popup shows free plan status
const $profileButton = testWindow.$("#user-profile-button");
$profileButton.trigger('click');
await popupToAppear(PROFILE_POPUP);
await verifyProfilePopupContent(VIEW_PHOENIX_FREE,
"free plan user profile popup for user with expired entitlements");

// Close popup
$profileButton.trigger('click');

// Perform logout
await performFullLogoutFlow();

// Verify pro branding remains false after logout
await verifyProBranding(false, "no pro branding after logout with expired entitlements");

// Check profile popup (signed out state)
$profileButton.trigger('click');
await popupToAppear(SIGNIN_POPUP);
// Not logged in user with no trial - no special branding expected
expect(testWindow.$(`.profile-popup .trial-plan-info`).length).toBe(0);

// Close popup
$profileButton.trigger('click');
});

it("should show trial user popup when entitlements are expired (active trial)", async function () {
console.log("llgT: Starting expired entitlements with active trial test");

// Setup: Expired pro subscription + active trial (10 days)
setupProUserMock(true, true);
await setupTrialState(10);

// Verify initial state shows pro branding due to trial (overrides expired entitlements)
await verifyProBranding(true, "pro branding initially due to active trial");

// Perform login
await performFullLoginFlow();

// Verify pro branding remains after login (trial overrides expired server entitlements)
await verifyProBranding(true, "pro branding after login - trial overrides expired entitlements");

// Check profile popup shows trial status (not expired server entitlements)
const $profileButton = testWindow.$("#user-profile-button");
$profileButton.trigger('click');
await popupToAppear(PROFILE_POPUP);
await verifyProfilePopupContent(VIEW_TRIAL_DAYS_LEFT,
"trial user profile popup - trial overrides expired server entitlements");

// Close popup
$profileButton.trigger('click');

// Perform logout
await performFullLogoutFlow();

// Verify pro branding remains after logout (trial continues)
await verifyProBranding(true, "pro branding after logout - trial still active");

// Check profile popup still shows trial status
$profileButton.trigger('click');
await popupToAppear(SIGNIN_POPUP);
await verifyProfilePopupContent(VIEW_TRIAL_DAYS_LEFT,
"trial user profile popup for logged out user");

// Close popup
$profileButton.trigger('click');
});
}

exports.setup = setup;
Expand Down
Loading
Loading