diff --git a/src/services/login-browser.js b/src/services/login-browser.js index 284f1495e9..3c0498d8b1 100644 --- a/src/services/login-browser.js +++ b/src/services/login-browser.js @@ -288,6 +288,9 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.AUTH, "browserLogin", "browser"); }, 1500); } + // on login we fire an entitlements changed event forcefully as new user came online + // even if entitlements didn't change(entitlements may not change between trial users for eg.). + LoginService._debounceEntitlementsChanged(); } function _cancelLoginWaiting() { diff --git a/src/services/login-desktop.js b/src/services/login-desktop.js index 251fff301e..5b595a5112 100644 --- a/src/services/login-desktop.js +++ b/src/services/login-desktop.js @@ -320,13 +320,16 @@ define(function (require, exports, module) { if(resolveResponse.userDetails) { // the user has validated the creds userProfile = resolveResponse.userDetails; + isLoggedInUser = true; ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_API, JSON.stringify(userProfile)); // bump the version so that in multi windows, the other window gets notified of the change PreferencesManager.stateManager.set(PREF_USER_PROFILE_VERSION, crypto.randomUUID()); checkAgain = false; - isLoggedInUser = true; dialog.close(); + // on login we fire an entitlements changed event forcefully as new user came online + // even if entitlements didn't change(entitlements may not change between trial users for eg.). + LoginService._debounceEntitlementsChanged(); } } catch (e) { console.error("Failed to check login status.", e); diff --git a/src/services/login-service.js b/src/services/login-service.js index d1980ac8a0..96ec7f5bf1 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -397,6 +397,7 @@ define(function (require, exports, module) { } // Reset device license state so it's re-evaluated on next entitlement check deviceLicensePrimed = false; + await _clearCachedEntitlements(); } @@ -708,6 +709,7 @@ define(function (require, exports, module) { LoginService.isLicensedDevice = isLicensedDevice; LoginService.isLicensedDeviceSystemWide = isLicensedDeviceSystemWide; LoginService.getDeviceID = getDeviceID; + LoginService._debounceEntitlementsChanged = _debounceEntitlementsChanged; LoginService.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED; async function handleReinstallCreds() { diff --git a/src/utils/Metrics.js b/src/utils/Metrics.js index 7f5e21e720..46fa0142fc 100644 --- a/src/utils/Metrics.js +++ b/src/utils/Metrics.js @@ -39,6 +39,12 @@ * @module utils/Metrics */ 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("Metrics should have access to KernalModeTrust. Cannot boot without trust ring"); + } + const MAX_AUDIT_ENTRIES = 3000, ONE_DAY = 24 * 60* 60 * 1000; let initDone = false, @@ -46,7 +52,7 @@ define(function (require, exports, module) { loggedDataForAudit = new Map(); let isFirstUseDay; - let userID, isPowerUserFn; + let userID, isPowerUserFn, powerUserPrefix; let cachedIsPowerUser = false; function _setUserID() { @@ -260,6 +266,25 @@ define(function (require, exports, module) { document.getElementsByTagName('head')[0].appendChild(script); } + async function _setPowerUserPrefix() { + powerUserPrefix = null; + const EntitlementsManager = KernalModeTrust.EntitlementsManager; + if(cachedIsPowerUser){ + // A power user is someone who used Phoenix at least 3 days/8 hours in the last two weeks + powerUserPrefix = "P"; + } else if(!isFirstUseDay){ + // A repeat user is a user who has used phoenix at least one other day before + powerUserPrefix = "R"; + } + if(EntitlementsManager.isLoggedIn()){ + if(await EntitlementsManager.isPaidSubscriber()){ + powerUserPrefix = "S"; // subscriber + return; + } + powerUserPrefix = "L"; // logged in user + } + } + /** * We are transitioning to our own analytics instead of google as we breached the free user threshold of google * and paid plans for GA starts at 100,000 USD. @@ -275,10 +300,14 @@ define(function (require, exports, module) { if (initOptions.isPowerUserFn) { isPowerUserFn = initOptions.isPowerUserFn; cachedIsPowerUser = isPowerUserFn(); // only call once to avoid heavy computations repeatedly + _setPowerUserPrefix(); setInterval(()=>{ cachedIsPowerUser = isPowerUserFn(); + _setPowerUserPrefix(); }, ONE_DAY); } + KernalModeTrust.EntitlementsManager.on(KernalModeTrust.EntitlementsManager.EVENT_ENTITLEMENTS_CHANGED, + _setPowerUserPrefix); } // some events generate too many ga events that ga can't handle. ignore them. @@ -363,12 +392,9 @@ define(function (require, exports, module) { * @type {function} */ function countEvent(eventType, eventCategory, eventSubCategory, count= 1) { - if(cachedIsPowerUser){ + if(powerUserPrefix){ // emit power user metrics too - _countEvent(`P-${eventType}`, eventCategory, eventSubCategory, count); - } else if(!isFirstUseDay){ - // emit repeat user metrics too - _countEvent(`R-${eventType}`, eventCategory, eventSubCategory, count); + _countEvent(`${powerUserPrefix}-${eventType}`, eventCategory, eventSubCategory, count); } _countEvent(eventType, eventCategory, eventSubCategory, count); } @@ -394,12 +420,9 @@ define(function (require, exports, module) { * @type {function} */ function valueEvent(eventType, eventCategory, eventSubCategory, value) { - if(cachedIsPowerUser){ + if(powerUserPrefix){ // emit power user metrics too - _valueEvent(`P-${eventType}`, eventCategory, eventSubCategory, value); - } else if(!isFirstUseDay){ - // emit repeat user metrics too - _valueEvent(`R-${eventType}`, eventCategory, eventSubCategory, value); + _valueEvent(`${powerUserPrefix}-${eventType}`, eventCategory, eventSubCategory, value); } _valueEvent(eventType, eventCategory, eventSubCategory, value); } diff --git a/test/spec/login-browser-integ-test.js b/test/spec/login-browser-integ-test.js index 58faf469de..7d7ec32023 100644 --- a/test/spec/login-browser-integ-test.js +++ b/test/spec/login-browser-integ-test.js @@ -40,6 +40,7 @@ define(function (require, exports, module) { LoginBrowserExports, ProDialogsExports, EntitlementsExports, + entitlementsService, originalOpen, originalFetch; @@ -77,6 +78,9 @@ define(function (require, exports, module) { LoginBrowserExports = testWindow._test_login_browser_exports; ProDialogsExports = testWindow._test_pro_dlg_login_exports; EntitlementsExports = testWindow._test_entitlements_exports; + entitlementsService = EntitlementsExports.EntitlementsService; + entitlementsService.on(entitlementsService.EVENT_ENTITLEMENTS_CHANGED, + LoginShared.entitlmentsChangedHandler); // Store original functions for restoration originalOpen = testWindow.open; @@ -109,6 +113,8 @@ define(function (require, exports, module) { afterAll(async function () { // Restore original functions + entitlementsService.off(entitlementsService.EVENT_ENTITLEMENTS_CHANGED, + LoginShared.entitlmentsChangedHandler); testWindow.open = originalOpen; // Restore all fetch function overrides diff --git a/test/spec/login-desktop-integ-test.js b/test/spec/login-desktop-integ-test.js index b578720187..1ac82618a2 100644 --- a/test/spec/login-desktop-integ-test.js +++ b/test/spec/login-desktop-integ-test.js @@ -47,6 +47,7 @@ define(function (require, exports, module) { LoginDesktopExports, ProDialogsExports, EntitlementsExports, + entitlementsService, originalOpenURLInDefaultBrowser, originalCopyToClipboard, originalFetch; @@ -84,6 +85,9 @@ define(function (require, exports, module) { LoginDesktopExports = testWindow._test_login_desktop_exports; ProDialogsExports = testWindow._test_pro_dlg_login_exports; EntitlementsExports = testWindow._test_entitlements_exports; + entitlementsService = EntitlementsExports.EntitlementsService; + entitlementsService.on(entitlementsService.EVENT_ENTITLEMENTS_CHANGED, + LoginShared.entitlmentsChangedHandler); // Store original functions for restoration originalOpenURLInDefaultBrowser = testWindow.Phoenix.app.openURLInDefaultBrowser; @@ -117,6 +121,8 @@ define(function (require, exports, module) { afterAll(async function () { // Restore original functions + entitlementsService.off(entitlementsService.EVENT_ENTITLEMENTS_CHANGED, + LoginShared.entitlmentsChangedHandler); testWindow.Phoenix.app.openURLInDefaultBrowser = originalOpenURLInDefaultBrowser; testWindow.Phoenix.app.copyToClipboard = originalCopyToClipboard; diff --git a/test/spec/login-shared.js b/test/spec/login-shared.js index b6bfd79583..8bbe4b4d75 100644 --- a/test/spec/login-shared.js +++ b/test/spec/login-shared.js @@ -289,9 +289,16 @@ define(function (require, exports, module) { EntitlementsExports = _EntitlementsExports; } + let entitlementsEventFired = false; + function entitlmentsChangedHandler() { + entitlementsEventFired = true; + } + function setupSharedTests() { it("should complete login and logout flow", async function () { + entitlementsEventFired = false; + // Setup basic user mock setupProUserMock(false); @@ -299,10 +306,21 @@ define(function (require, exports, module) { await performFullLoginFlow(); expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(true); + // Wait for entitlements event to fire after login + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after login"); + expect(entitlementsEventFired).toBe(true); + + // Reset flag for logout test + entitlementsEventFired = false; + // Perform full logout flow await performFullLogoutFlow(); expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(false); verifyProfileIconBlanked(); + + // Wait for entitlements event to fire after logout + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after logout"); + expect(entitlementsEventFired).toBe(true); }); it("should update profile icon after login", async function () { @@ -392,6 +410,8 @@ define(function (require, exports, module) { it("should show pro branding for user with pro subscription (expired trial)", async function () { console.log("llgT: Starting pro user with expired trial test"); + entitlementsEventFired = false; + // Setup: Pro subscription + expired trial setupProUserMock(true); await setupExpiredTrial(); @@ -408,6 +428,10 @@ define(function (require, exports, module) { await performFullLoginFlow(); await verifyProBranding(true, "pro branding to appear after pro user login"); + // Wait for entitlements event to fire after login + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after login"); + expect(entitlementsEventFired).toBe(true); + // Verify entitlements API consistency for logged in pro user await verifyIsInProTrialEntitlement(false, "pro user should not be in trial"); await verifyPlanEntitlements({ isSubscriber: true, paidSubscriber: true }, "pro user should have paid subscriber plan"); @@ -423,9 +447,16 @@ define(function (require, exports, module) { // Close popup $profileButton.trigger('click'); + // Reset flag for logout test + entitlementsEventFired = false; + // Perform logout await performFullLogoutFlow(); + // Wait for entitlements event to fire after logout + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after logout"); + expect(entitlementsEventFired).toBe(true); + // For user with pro subscription + expired trial: // After logout, pro branding should disappear because: // 1. No server entitlements (logged out) @@ -441,6 +472,8 @@ define(function (require, exports, module) { it("should show trial branding for user without pro subscription (active trial)", async function () { console.log("llgT: Starting trial user test"); + entitlementsEventFired = false; + // Setup: No pro subscription + active trial (15 days) setupProUserMock(false); await setupTrialState(15); @@ -459,6 +492,10 @@ define(function (require, exports, module) { // Verify pro branding remains after login await verifyProBranding(true, "after trial user login"); + // Wait for entitlements event to fire after login + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after login"); + expect(entitlementsEventFired).toBe(true); + // Verify entitlements API consistency for logged in trial user await verifyIsInProTrialEntitlement(true, "user should still be in trial after login"); await verifyPlanEntitlements({ isSubscriber: true, paidSubscriber: false }, "trial user should have isSubscriber true but paidSubscriber false"); @@ -474,9 +511,16 @@ define(function (require, exports, module) { // Close popup $profileButton.trigger('click'); + // Reset flag for logout test + entitlementsEventFired = false; + // Perform logout await performFullLogoutFlow(); + // Wait for entitlements event to fire after logout + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after logout"); + expect(entitlementsEventFired).toBe(true); + // Verify pro branding remains after logout (trial continues) await verifyProBranding(true, "Trial branding to remain after logout"); @@ -541,6 +585,8 @@ define(function (require, exports, module) { it("should show free branding for user without pro subscription (expired trial)", async function () { console.log("llgT: Starting desktop trial user test"); + entitlementsEventFired = false; + // Setup: No pro subscription + expired trial setupProUserMock(false); await setupExpiredTrial(); @@ -562,6 +608,10 @@ define(function (require, exports, module) { // Verify pro branding remains after login await verifyProBranding(false, "after trial free user login"); + // Wait for entitlements event to fire after login + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after login"); + expect(entitlementsEventFired).toBe(true); + // Verify entitlements API consistency for logged in free user await verifyPlanEntitlements({ isSubscriber: false, paidSubscriber: false, name: testWindow.Strings.USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE }, "free plan for logged in user with expired trial"); @@ -579,9 +629,16 @@ define(function (require, exports, module) { // Close popup $profileButton.trigger('click'); + // Reset flag for logout test + entitlementsEventFired = false; + // Perform logout await performFullLogoutFlow(); + // Wait for entitlements event to fire after logout + await awaitsFor(() => entitlementsEventFired, "Entitlements event to fire after logout"); + expect(entitlementsEventFired).toBe(true); + // Verify pro branding remains after logout (trial continues) await verifyProBranding(false, "Trial branding to remain after logout"); @@ -921,6 +978,7 @@ define(function (require, exports, module) { exports.popupToAppear = popupToAppear; exports.performFullLogoutFlow = performFullLogoutFlow; exports.verifyProfileIconBlanked = verifyProfileIconBlanked; + exports.entitlmentsChangedHandler = entitlmentsChangedHandler; exports.VIEW_TRIAL_DAYS_LEFT = VIEW_TRIAL_DAYS_LEFT; exports.VIEW_PHOENIX_PRO = VIEW_PHOENIX_PRO; exports.VIEW_PHOENIX_FREE = VIEW_PHOENIX_FREE;