From 543d69b55b5e457251c0fd6c3c10f8f79c426547 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 28 Sep 2025 11:56:55 +0530 Subject: [PATCH 1/4] chore: pass platform and appType to getAppEntitlements api --- src/services/login-service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/login-service.js b/src/services/login-service.js index 094c63fdf..39e4864c5 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -253,7 +253,8 @@ define(function (require, exports, module) { const accountBaseURL = LoginService.getAccountBaseURL(); const language = brackets.getLocale(); const currentVersion = window.AppConfig.apiVersion || "1.0.0"; - let url = `${accountBaseURL}/getAppEntitlements?lang=${language}&version=${currentVersion}`; + let url = `${accountBaseURL}/getAppEntitlements?lang=${language}&version=${currentVersion}`+ + `&platform=${Phoenix.platform}&appType=${Phoenix.isNativeApp ? "desktop" : "browser"}}`; let fetchOptions = { method: 'GET', headers: { From a6d6b304224de5ce2a78e4780f831dbf6b582694 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 28 Sep 2025 16:31:01 +0530 Subject: [PATCH 2/4] fix: entitlements were not getting refreshed evry 10 minutes --- src/services/login-service.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/services/login-service.js b/src/services/login-service.js index 39e4864c5..e603c83fe 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -353,21 +353,40 @@ define(function (require, exports, module) { /** * Start the 10-minute interval timer for monitoring entitlements */ - function startEntitlementsMonitor() { + function startEffectiveEntitlementsMonitor() { + // Reconcile effective entitlements from server. So the effective entitlements api injects trial + // entitlements data. but only the server fetch will trigger the entitlements change event. + // so in here, we observe the effective entitlements, and if the effective entitlements are changed, + // since the last triggered state, we trigger a change event. This only concerens with the effective + // entitlement changes. This will not logout the user if user logged out from the server admin panel, + // but his entitlements will be cleared by this call anyways. + + // At app start we refresh entitlements, then only one each user action like user clicks on profile icon, + // or if some user hits some backend api, we will refresh entitlements. But here, we periodically refresh + // entitlements from the server every 10 minutes, but only trigger entitlement change events only if some + // effective entitlement(Eg. trial) data changed or any validity expired. + if(Phoenix.isTestWindow){ + return; + } + setTimeout( async function() { + lastRecordedState = await getEffectiveEntitlements(false); + }, 30000); setInterval(async () => { try { - const current = await getEffectiveEntitlements(false); // Get effective entitlements + // Get fresh effective entitlements + const freshEntitlements = await getEffectiveEntitlements(true); // Check if we need to refresh - const expiredPlanName = KernalModeTrust.LoginUtils.validTillExpired(current, lastRecordedState); - const hasChanged = KernalModeTrust.LoginUtils.haveEntitlementsChanged(current, lastRecordedState); + const expiredPlanName = KernalModeTrust.LoginUtils + .validTillExpired(freshEntitlements, lastRecordedState); + const hasChanged = KernalModeTrust.LoginUtils + .haveEntitlementsChanged(freshEntitlements, 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. @@ -375,7 +394,7 @@ define(function (require, exports, module) { } // Update last recorded state - lastRecordedState = current; + lastRecordedState = freshEntitlements; } catch (error) { console.error('Entitlements monitor error:', error); } @@ -598,7 +617,7 @@ define(function (require, exports, module) { } // Start the entitlements monitor timer - startEntitlementsMonitor(); + startEffectiveEntitlementsMonitor(); exports.init = init; // no public exports to prevent extension tampering From fdf6132bd638c031703f4bc65c77d8efa1364918 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 28 Sep 2025 16:33:18 +0530 Subject: [PATCH 3/4] fix: entitlements were not getting refreshed evry 10 minutes --- src/services/login-service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/login-service.js b/src/services/login-service.js index e603c83fe..b9255105d 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -369,6 +369,8 @@ define(function (require, exports, module) { return; } setTimeout( async function() { + // prime the entitlement monitor with the current effective entitlements, after app start, the system would + // have resolved any existing login info by now and effective entitlements would be available if any. lastRecordedState = await getEffectiveEntitlements(false); }, 30000); setInterval(async () => { From 2538d37440b34634ee5b1a441a4037b392f41d24 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 30 Sep 2025 13:11:01 +0530 Subject: [PATCH 4/4] chore: show descriptive plan names from server in profile popup --- src/nls/root/strings.js | 2 +- src/services/entitlements.js | 2 +- src/services/login-service.js | 9 ++++++++- src/services/profile-menu.js | 12 +++++++----- test/spec/login-browser-integ-test.js | 4 +++- test/spec/login-desktop-integ-test.js | 4 +++- test/spec/login-shared.js | 6 +++--- test/spec/login-utils-test.js | 2 +- test/spec/promotions-integ-test.js | 4 ++-- 9 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 3770cad5b..3910de0ff 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1690,5 +1690,5 @@ define({ "PROMO_PRO_UNLOCK_MESSAGE": "Subscribe now to unlock these advanced features:", "PROMO_PRO_TRIAL_DAYS_LEFT": "Phoenix Pro Trial ({0} days left)", "GET_PHOENIX_PRO": "Get Phoenix Pro", - "USER_FREE_PLAN_NAME": "Free Plan" + "USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE": "Community Edition" }); diff --git a/src/services/entitlements.js b/src/services/entitlements.js index a04f6fd46..6324b76ef 100644 --- a/src/services/entitlements.js +++ b/src/services/entitlements.js @@ -97,7 +97,7 @@ define(function (require, exports, module) { const currentDate = Date.now(); return { paidSubscriber: false, - name: Strings.USER_FREE_PLAN_NAME, + name: Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE, validTill: currentDate + (FREE_PLAN_VALIDITY_DAYS * MS_IN_DAY) }; } diff --git a/src/services/login-service.js b/src/services/login-service.js index b9255105d..ae3404e88 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -416,7 +416,8 @@ define(function (require, exports, module) { entitlements.plan = { ...entitlements.plan, paidSubscriber: false, - name: Strings.USER_FREE_PLAN_NAME, + name: Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE, + fullName: Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE, validTill: currentDate + (FREE_PLAN_VALIDITY_DAYS * MS_IN_DAY) }; } @@ -453,6 +454,8 @@ define(function (require, exports, module) { * plan: { * paidSubscriber: true, // Always true for trial users * name: "Phoenix Pro" + * fullName: "Phoenix Pro" // this can be deceptive name like "Phoenix Pro For Education" to use in + * // profile popup, not main branding * }, * isInProTrial: true, // Indicates this is a trial user * trialDaysRemaining: number, // Days left in trial @@ -478,6 +481,8 @@ define(function (require, exports, module) { * lang: string, * plan: { * name: "Phoenix Pro", + * fullName: "Phoenix Pro" // this can be deceptive name like "Phoenix Pro For Education" to use in + * // profile popup, not main branding * paidSubscriber: boolean, * validTill: number // Timestamp * }, @@ -553,6 +558,7 @@ define(function (require, exports, module) { ...serverEntitlements.plan, paidSubscriber: true, name: brackets.config.main_pro_plan, + fullName: brackets.config.main_pro_plan, validTill: dateNowFn() + trialDaysRemaining * MS_IN_DAY }, isInProTrial: true, @@ -574,6 +580,7 @@ define(function (require, exports, module) { plan: { paidSubscriber: true, name: brackets.config.main_pro_plan, + fullName: brackets.config.main_pro_plan, validTill: dateNowFn() + trialDaysRemaining * MS_IN_DAY }, isInProTrial: true, diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index f4a150cc1..d5a69ce35 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -217,7 +217,8 @@ define(function (require, exports, module) { // Use kernal mode apis for trusted check of pro features. Phoenix.pro.plan = { paidSubscriber: false, - name: "Community Edition" + name: Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE, + fullName: Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE }; } @@ -225,11 +226,12 @@ define(function (require, exports, module) { Phoenix.pro.plan = { paidSubscriber: entitlements.plan.paidSubscriber, name: entitlements.plan.name, + fullName: entitlements.plan.fullName, validTill: entitlements.plan.validTill }; } if (entitlements && entitlements.plan && entitlements.plan.paidSubscriber) { - // Pro user (paid subscriber or trial): show plan name with feather icon + // Pro user (paid subscriber or trial): show short name branding with `name feather icon`(not full name) let displayName = entitlements.plan.name || brackets.config.main_pro_plan; if (entitlements.isInProTrial) { displayName = brackets.config.main_pro_plan; // Just "Phoenix Pro" for branding, not "Phoenix Pro Trial" @@ -379,7 +381,7 @@ define(function (require, exports, module) { } else { // For paid users: regular plan name with icon const proTitle = ` - ${entitlements.plan.name} + ${entitlements.plan.fullName} `; $planName.addClass('user-plan-paid').html(proTitle); @@ -387,7 +389,7 @@ define(function (require, exports, module) { } } else { // Use simple text for free users - $planName.addClass('user-plan-free').text(entitlements.plan.name); + $planName.addClass('user-plan-free').text(entitlements.plan.fullName); } } else { $getProLink.removeClass('forced-hidden'); @@ -436,7 +438,7 @@ define(function (require, exports, module) { initials: profileData.profileIcon.initials, avatarColor: profileData.profileIcon.color, planClass: "user-plan-free", - planName: Strings.USER_FREE_PLAN_NAME, + planName: Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE, titleText: "Ai Quota Used", usageText: "100 / 200 credits", usedPercent: 0, diff --git a/test/spec/login-browser-integ-test.js b/test/spec/login-browser-integ-test.js index 80e52adf1..1e48af61d 100644 --- a/test/spec/login-browser-integ-test.js +++ b/test/spec/login-browser-integ-test.js @@ -185,6 +185,7 @@ define(function (require, exports, module) { entitlementsResponse.plan = { paidSubscriber: true, name: "Phoenix Pro", + fullName: "Phoenix Pro", validTill: validTill }; entitlementsResponse.entitlements = { @@ -196,7 +197,8 @@ define(function (require, exports, module) { } else { entitlementsResponse.plan = { paidSubscriber: false, - name: "Free Plan" + name: "Free Plan", + fullName: "Free Plan" }; } diff --git a/test/spec/login-desktop-integ-test.js b/test/spec/login-desktop-integ-test.js index 6a48bc8ff..3a1c7bc32 100644 --- a/test/spec/login-desktop-integ-test.js +++ b/test/spec/login-desktop-integ-test.js @@ -207,6 +207,7 @@ define(function (require, exports, module) { entitlementsResponse.plan = { paidSubscriber: true, name: "Phoenix Pro", + fullName: "Phoenix Pro", validTill: validTill }; entitlementsResponse.entitlements = { @@ -218,7 +219,8 @@ define(function (require, exports, module) { } else { entitlementsResponse.plan = { paidSubscriber: false, - name: "Free Plan" + name: "Free Plan", + fullName: "Free Plan" }; } diff --git a/test/spec/login-shared.js b/test/spec/login-shared.js index 60f2c0c51..5c3565f3d 100644 --- a/test/spec/login-shared.js +++ b/test/spec/login-shared.js @@ -492,7 +492,7 @@ define(function (require, exports, module) { await verifyProBranding(false, "no pro branding to start with"); // Verify entitlements API consistency for logged out user with expired trial - await verifyPlanEntitlements({ paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME }, + await verifyPlanEntitlements({ paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE }, "free plan for logged out user with expired trial"); await verifyIsInProTrialEntitlement(false, "no trial for user with expired trial"); await verifyTrialRemainingDaysEntitlement(0, "no trial days remaining for expired trial"); @@ -505,7 +505,7 @@ define(function (require, exports, module) { await verifyProBranding(false, "after trial free user login"); // Verify entitlements API consistency for logged in free user - await verifyPlanEntitlements({ paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME }, + await verifyPlanEntitlements({ paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE }, "free plan for logged in user with expired trial"); await verifyIsInProTrialEntitlement(false, "still no trial after login"); await verifyLiveEditEntitlement({ activated: false }, "live edit still deactivated after login"); @@ -551,7 +551,7 @@ define(function (require, exports, module) { await verifyProBranding(false, "no pro branding initially due to expired entitlements"); // Verify entitlements API consistency for logged out user with no trial - await verifyPlanEntitlements({ paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME }, + await verifyPlanEntitlements({ paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE }, "free plan for logged out user with no trial"); await verifyIsInProTrialEntitlement(false, "no trial for logged out user"); await verifyTrialRemainingDaysEntitlement(0, "no trial days remaining"); diff --git a/test/spec/login-utils-test.js b/test/spec/login-utils-test.js index 8d05cd14c..895a9ac16 100644 --- a/test/spec/login-utils-test.js +++ b/test/spec/login-utils-test.js @@ -497,4 +497,4 @@ define(function (require, exports, module) { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/spec/promotions-integ-test.js b/test/spec/promotions-integ-test.js index a643679fe..aae860814 100644 --- a/test/spec/promotions-integ-test.js +++ b/test/spec/promotions-integ-test.js @@ -818,7 +818,7 @@ define(function (require, exports, module) { LoginServiceExports._validateAndFilterEntitlements(expiredPlanEntitlements); expect(expiredPlanEntitlements.plan.paidSubscriber).toBe(false); - expect(expiredPlanEntitlements.plan.name).toBe(testWindow.Strings.USER_FREE_PLAN_NAME); + expect(expiredPlanEntitlements.plan.name).toBe(testWindow.Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE); expect(expiredPlanEntitlements.plan.validTill).toBeGreaterThan(mockNow); // Test valid plan remains unchanged @@ -848,7 +848,7 @@ define(function (require, exports, module) { LoginServiceExports._validateAndFilterEntitlements(noValidTillEntitlements); expect(noValidTillEntitlements.plan.paidSubscriber).toBe(false); - expect(noValidTillEntitlements.plan.name).toBe(testWindow.Strings.USER_FREE_PLAN_NAME); + expect(noValidTillEntitlements.plan.name).toBe(testWindow.Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE); }); it("should validate and filter expired feature entitlements", function () {