From 27687b9c2f1c9f1eb6e77ffb972e4c9b8efb66cb Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 23 Oct 2025 19:48:15 +0530 Subject: [PATCH 1/3] chore: power user survey for subscribers and ligged in users support --- .../Phoenix/guided-tour.js | 51 +++++++++++-------- src/services/EntitlementsManager.js | 17 ++++++- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/extensionsIntegrated/Phoenix/guided-tour.js b/src/extensionsIntegrated/Phoenix/guided-tour.js index aaeb97e634..217f70be45 100644 --- a/src/extensionsIntegrated/Phoenix/guided-tour.js +++ b/src/extensionsIntegrated/Phoenix/guided-tour.js @@ -1,24 +1,15 @@ -/* - * 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. - * - */ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2021 - present core.ai. All rights reserved. + +/*global logger*/ 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("Guided Tour should have access to KernalModeTrust. Cannot boot without trust ring"); + } + const NotificationUI = require("widgets/NotificationUI"), Commands = require("command/Commands"), Strings = require("strings"), @@ -244,6 +235,23 @@ define(function (require, exports, module) { PhStore.setItem(GUIDED_TOUR_LOCAL_STORAGE_KEY, JSON.stringify(userAlreadyDidAction)); } + async function _resolvePowerUserSurveyURL(surveyJson) { + try { + const isLoggedIn = KernalModeTrust.EntitlementsManager.isLoggedIn(); + if(!isLoggedIn) { + return surveyJson.powerUser; + } + const paidSubscriber = await KernalModeTrust.EntitlementsManager.isPaidSubscriber(); + if(paidSubscriber && surveyJson.powerUserPaid) { + return surveyJson.powerUserPaid; + } + return surveyJson.powerUserLoggedIn || surveyJson.powerUser; + } catch (e) { + logger.reportError(e, "Error resolving power user survey URL"); + } + return surveyJson.powerUser; + } + async function _showSurveys() { try { if(!navigator.onLine){ @@ -258,6 +266,8 @@ define(function (require, exports, module) { newUserShowDelayMS: surveyJSON.browser.newUserShowDelayMS || surveyJSON.newUserShowDelayMS, newUserUseDialog: surveyJSON.browser.newUserUseDialog || surveyJSON.newUserUseDialog, powerUser: surveyJSON.browser.powerUser || surveyJSON.powerUser, + powerUserPaid: surveyJSON.browser.powerUserPaid || surveyJSON.powerUserPaid, + powerUserLoggedIn: surveyJSON.browser.powerUserLoggedIn || surveyJSON.powerUserLoggedIn, powerUserTitle: surveyJSON.browser.powerUserTitle || surveyJSON.powerUserTitle, powerUserShowIntervalDays: surveyJSON.browser.powerUserShowIntervalDays || surveyJSON.powerUserShowIntervalDays, @@ -266,7 +276,8 @@ define(function (require, exports, module) { } surveyJSON.newUser && _showFirstUseSurvey(surveyJSON.newUser, surveyJSON.newUserShowDelayMS, surveyJSON.newUserTitle, surveyJSON.newUserUseDialog); - surveyJSON.powerUser && _showRepeatUserSurvey(surveyJSON.powerUser, surveyJSON.powerUserShowIntervalDays, + const powerUserSurveyURL = await _resolvePowerUserSurveyURL(surveyJSON); + powerUserSurveyURL && _showRepeatUserSurvey(powerUserSurveyURL, surveyJSON.powerUserShowIntervalDays, surveyJSON.powerUserTitle, surveyJSON.powerUserUseDialog); } catch (e) { console.error("Error fetching survey link", surveyLinksURL, e); diff --git a/src/services/EntitlementsManager.js b/src/services/EntitlementsManager.js index fb5c9d512c..964b494072 100644 --- a/src/services/EntitlementsManager.js +++ b/src/services/EntitlementsManager.js @@ -55,7 +55,7 @@ define(function (require, exports, module) { * @returns {*} */ function isLoggedIn() { - return LoginService.isLoggedIn(); + return LoginService && LoginService.isLoggedIn(); } /** @@ -109,6 +109,19 @@ define(function (require, exports, module) { }; } + /** + * Checks if the current user is a paid subscriber (has purchased a plan, not trial users) + * + * @return {Promise} A promise that resolves to true if the user is a paid subscriber, false otherwise. + */ + async function isPaidSubscriber() { + if(!isLoggedIn()) { + return false; + } + const planDetails = await getPlanDetails(); + return !!planDetails.paidSubscriber; + } + /** * Check if user is in a pro trial. IF the user is in pro trail, then `plan.isSubscriber` will always be true. * @returns {Promise} True if user is in pro trial, false otherwise @@ -339,6 +352,7 @@ define(function (require, exports, module) { EntitlementsService: EntitlementsManager, isLoggedIn, getPlanDetails, + isPaidSubscriber, isInProTrial, getTrialRemainingDays, getRawEntitlements, @@ -355,6 +369,7 @@ define(function (require, exports, module) { EntitlementsManager.isLoggedIn = isLoggedIn; EntitlementsManager.loginToAccount = loginToAccount; EntitlementsManager.getPlanDetails = getPlanDetails; + EntitlementsManager.isPaidSubscriber = isPaidSubscriber; EntitlementsManager.isInProTrial = isInProTrial; EntitlementsManager.getTrialRemainingDays = getTrialRemainingDays; EntitlementsManager.getRawEntitlements = getRawEntitlements; From 420674ec12895c3341c27f5bcf001a8597b49b72 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 23 Oct 2025 20:07:51 +0530 Subject: [PATCH 2/3] test: integ tests for EntitlementsManager.isPaidSubscriber api --- test/spec/login-shared.js | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/spec/login-shared.js b/test/spec/login-shared.js index 4f246f6122..0c6d4a64dd 100644 --- a/test/spec/login-shared.js +++ b/test/spec/login-shared.js @@ -244,6 +244,11 @@ define(function (require, exports, module) { } } + async function verifyIsPaidSubscriber(expected, _testDescription) { + const isPaidSub = await EntitlementsExports.isPaidSubscriber(); + expect(isPaidSub).toBe(expected); + } + async function verifyRawEntitlements(expected, _testDescription) { const rawEntitlements = await EntitlementsExports.getRawEntitlements(); @@ -395,6 +400,7 @@ define(function (require, exports, module) { // Verify entitlements API consistency for logged out state await verifyIsInProTrialEntitlement(false, "no trial for logged out user with expired trial"); await verifyTrialRemainingDaysEntitlement(0, "no trial days remaining"); + await verifyIsPaidSubscriber(false, "logged out user should not be a paid subscriber"); // Perform login await performFullLoginFlow(); @@ -403,6 +409,7 @@ define(function (require, exports, module) { // 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"); + await verifyIsPaidSubscriber(true, "pro user should be a paid subscriber"); // Check profile popup shows pro status (not trial) const $profileButton = testWindow.$("#user-profile-button"); @@ -426,6 +433,7 @@ define(function (require, exports, module) { // Verify entitlements API consistency after logout await verifyRawEntitlements(null, "no raw entitlements when logged out"); await verifyIsInProTrialEntitlement(false, "no trial after logout"); + await verifyIsPaidSubscriber(false, "logged out pro user should not be a paid subscriber (no server entitlements)"); }); it("should show trial branding for user without pro subscription (active trial)", async function () { @@ -452,6 +460,7 @@ define(function (require, exports, module) { // 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"); + await verifyIsPaidSubscriber(false, "trial user should not be a paid subscriber"); // Check profile popup shows trial status const $profileButton = testWindow.$("#user-profile-button"); @@ -472,6 +481,7 @@ define(function (require, exports, module) { // Verify entitlements API consistency after logout (trial still active) await verifyIsInProTrialEntitlement(true, "trial should persist after logout"); await verifyRawEntitlements(null, "no raw entitlements when logged out"); + await verifyIsPaidSubscriber(false, "logged out trial user should not be a paid subscriber"); // Check profile popup still shows trial status $profileButton.trigger('click'); @@ -496,6 +506,9 @@ define(function (require, exports, module) { // Verify pro branding appears await verifyProBranding(true, "Pro branding to appear for pro user"); + // Verify entitlements API consistency for logged in pro user with trial + await verifyIsPaidSubscriber(true, "pro user with trial should be a paid subscriber"); + // Check profile popup shows pro status (not trial text) const $profileButton = testWindow.$("#user-profile-button"); $profileButton.trigger('click'); @@ -513,6 +526,7 @@ define(function (require, exports, module) { // Verify pro branding remains due to trial (even though subscription is gone) await verifyProBranding(true, "Pro branding should remain after logout as trial user"); + await verifyIsPaidSubscriber(false, "logged out user should not be a paid subscriber (no server entitlements)"); $profileButton.trigger('click'); await popupToAppear(SIGNIN_POPUP); await verifyProfilePopupContent(VIEW_TRIAL_DAYS_LEFT, @@ -538,6 +552,7 @@ define(function (require, exports, module) { await verifyIsInProTrialEntitlement(false, "no trial for user with expired trial"); await verifyTrialRemainingDaysEntitlement(0, "no trial days remaining for expired trial"); await verifyLiveEditEntitlement({ activated: false }, "live edit deactivated for expired trial"); + await verifyIsPaidSubscriber(false, "logged out free user should not be a paid subscriber"); // Perform login await performFullLoginFlow(); @@ -550,6 +565,7 @@ define(function (require, exports, module) { "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"); + await verifyIsPaidSubscriber(false, "logged in free user should not be a paid subscriber"); // Check profile popup shows free plan status const $profileButton = testWindow.$("#user-profile-button"); @@ -570,6 +586,7 @@ define(function (require, exports, module) { // Verify entitlements API consistency after logout await verifyRawEntitlements(null, "no raw entitlements when logged out"); await verifyIsInProTrialEntitlement(false, "no trial after logout"); + await verifyIsPaidSubscriber(false, "logged out free user should not be a paid subscriber"); // Check profile popup still shows free plan status as trial expired $profileButton.trigger('click'); @@ -598,6 +615,7 @@ define(function (require, exports, module) { await verifyTrialRemainingDaysEntitlement(0, "no trial days remaining"); await verifyRawEntitlements(null, "no raw entitlements when logged out"); await verifyLiveEditEntitlement({ activated: false }, "live edit deactivated with no trial"); + await verifyIsPaidSubscriber(false, "logged out user with expired entitlements should not be a paid subscriber"); // Perform login await performFullLoginFlow(); @@ -615,6 +633,7 @@ define(function (require, exports, module) { subscribeURL: testWindow.brackets.config.purchase_url, upgradeToPlan: testWindow.brackets.config.main_pro_plan }, "live edit deactivated with fallback URLs for expired entitlements"); + await verifyIsPaidSubscriber(false, "logged in user with expired entitlements should not be a paid subscriber"); // Check profile popup shows free plan status const $profileButton = testWindow.$("#user-profile-button"); @@ -635,6 +654,7 @@ define(function (require, exports, module) { // Verify entitlements API consistency after logout await verifyRawEntitlements(null, "no raw entitlements when logged out"); await verifyIsInProTrialEntitlement(false, "no trial after logout"); + await verifyIsPaidSubscriber(false, "logged out user with expired entitlements should not be a paid subscriber"); // Check profile popup (signed out state) $profileButton.trigger('click'); @@ -663,6 +683,7 @@ define(function (require, exports, module) { await verifyTrialRemainingDaysEntitlement(10, "should have 10 trial days remaining"); await verifyRawEntitlements(null, "no raw entitlements when logged out"); await verifyLiveEditEntitlement({ activated: true }, "live edit activated via trial"); + await verifyIsPaidSubscriber(false, "logged out trial user should not be a paid subscriber"); // Perform login await performFullLoginFlow(); @@ -676,6 +697,7 @@ define(function (require, exports, module) { await verifyIsInProTrialEntitlement(true, "user should still be in trial after login"); await verifyTrialRemainingDaysEntitlement(10, "trial days should remain 10 after login"); await verifyLiveEditEntitlement({ activated: true }, "live edit should be activated via trial override"); + await verifyIsPaidSubscriber(false, "logged in trial user should not be a paid subscriber (expired entitlements)"); // Check profile popup shows trial status (not expired server entitlements) const $profileButton = testWindow.$("#user-profile-button"); @@ -698,6 +720,7 @@ define(function (require, exports, module) { await verifyTrialRemainingDaysEntitlement(10, "trial days should persist after logout"); await verifyRawEntitlements(null, "no raw entitlements when logged out"); await verifyLiveEditEntitlement({ activated: true }, "live edit still activated via trial after logout"); + await verifyIsPaidSubscriber(false, "logged out trial user should not be a paid subscriber"); // Check profile popup still shows trial status $profileButton.trigger('click'); @@ -752,6 +775,47 @@ define(function (require, exports, module) { } }); + it("should test isPaidSubscriber API across different user states", async function () { + console.log("isPaidSubscriber: Testing API across different user states"); + + try { + // Test 1: Logged out user should not be a paid subscriber + await cleanupTrialState(); + expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(false); + await verifyIsPaidSubscriber(false, "logged out user should not be a paid subscriber"); + + // Test 2: Free user (no subscription) should not be a paid subscriber + setupProUserMock(false); + await performFullLoginFlow(); + expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(true); + await verifyIsPaidSubscriber(false, "free user should not be a paid subscriber"); + await performFullLogoutFlow(); + + // Test 3: Trial user (no paid subscription) should not be a paid subscriber + setupProUserMock(false); + await setupTrialState(10); + await performFullLoginFlow(); + expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(true); + await verifyIsPaidSubscriber(false, "trial user should not be a paid subscriber"); + await performFullLogoutFlow(); + await cleanupTrialState(); + + // Test 4: Pro user with paid subscription should be a paid subscriber + setupProUserMock(true); + await performFullLoginFlow(); + expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(true); + await verifyIsPaidSubscriber(true, "pro user should be a paid subscriber"); + + // Test 5: After logout, should not be a paid subscriber (no server entitlements) + await performFullLogoutFlow(); + expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(false); + await verifyIsPaidSubscriber(false, "logged out pro user should not be a paid subscriber"); + + } finally { + await cleanupTrialState(); + } + }); + if (Phoenix.isNativeApp) { it("should show device-licensed Pro branding and popup when not logged in", async function () { console.log("llgT: Starting device license Pro branding test"); @@ -797,6 +861,7 @@ define(function (require, exports, module) { ); await verifyIsInProTrialEntitlement(false, "device license is not a trial"); await verifyLiveEditEntitlement({ activated: true }, "live edit activated via device license"); + await verifyIsPaidSubscriber(false, "educational device license should not be a paid subscriber (not logged in)"); // Verify raw entitlements are present (not null) const rawEntitlements = await EntitlementsExports.getRawEntitlements(); From 774bed66a0d0d8f7e0bb10b3a8f68bbee7d4c38b Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 23 Oct 2025 20:48:16 +0530 Subject: [PATCH 3/3] fix: integ tests for Entitlments.isPaidSubscriber fails on desktop apps --- test/spec/login-shared.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/spec/login-shared.js b/test/spec/login-shared.js index 0c6d4a64dd..b6bfd79583 100644 --- a/test/spec/login-shared.js +++ b/test/spec/login-shared.js @@ -245,8 +245,10 @@ define(function (require, exports, module) { } async function verifyIsPaidSubscriber(expected, _testDescription) { - const isPaidSub = await EntitlementsExports.isPaidSubscriber(); - expect(isPaidSub).toBe(expected); + await awaitsFor(async ()=> { + const isPaidSub = await EntitlementsExports.isPaidSubscriber(); + return isPaidSub === expected; + }, `paid subscriber to be ${expected}`); } async function verifyRawEntitlements(expected, _testDescription) {