From eaeaf007b8eab61b948e904d702a8a3adfeeb78d Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 8 Sep 2025 11:18:08 +0530 Subject: [PATCH 01/10] fix: pro branding style in profile popup --- src/services/profile-menu.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index 89661ec641..827b1b3ae2 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -333,12 +333,21 @@ define(function (require, exports, module) { // Update plan information if (entitlements.plan) { const $planName = $popup.find('.user-plan-name'); - $planName.text(entitlements.plan.name); - - // Update plan class based on paid subscriber status + + // Update plan class and content based on paid subscriber status $planName.removeClass('user-plan-free user-plan-paid'); - const planClass = entitlements.plan.paidSubscriber ? 'user-plan-paid' : 'user-plan-free'; - $planName.addClass(planClass); + + if (entitlements.plan.paidSubscriber) { + // Use pro styling with feather icon for paid subscribers + const proTitle = ` + ${entitlements.plan.name} + + `; + $planName.addClass('user-plan-paid').html(proTitle); + } else { + // Use simple text for free users + $planName.addClass('user-plan-free').text(entitlements.plan.name); + } } // Update quota section if available From 030b638bbf365f05ca3529580a904a70e9ed9fdb Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 8 Sep 2025 22:52:37 +0530 Subject: [PATCH 02/10] chore: pro trial ui branding --- gulpfile.js/translateStrings.js | 9 ++- src/nls/root/strings.js | 3 +- src/services/html/login-popup.html | 8 ++ src/services/login-service.js | 49 ++++++++++++ src/services/profile-menu.js | 86 +++++++++++++++++----- src/services/promotions.js | 13 ++-- src/styles/brackets_core_ui_variables.less | 3 + src/styles/phoenix-pro.less | 8 ++ 8 files changed, 150 insertions(+), 29 deletions(-) diff --git a/gulpfile.js/translateStrings.js b/gulpfile.js/translateStrings.js index 5d7c2065d2..722e47d36c 100644 --- a/gulpfile.js/translateStrings.js +++ b/gulpfile.js/translateStrings.js @@ -80,9 +80,16 @@ function aggregateUtilizationMetrics(obj) { return globalUtilizationMetrics; } +const translationContext = +`This is a bunch of strings extracted from a JavaScript file used to develop our product with is a text editor. +Some strings may have HTML or templates(mustache library used). +The brand name “Phoenix Pro” must remain in English and should never be translated. +Please translate these strings accurately. +`; + function getTranslationrequest(stringsToTranslate, lang) { return { - translationContext: "This is a bunch of strings extracted from a JavaScript file used to develop our product with is a text editor. Some strings may have HTML or templates(mustache library used). Please translate these strings accurately.", + translationContext: translationContext, "source": stringsToTranslate, "provider": "vertex", "sourceContext": { diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index d9efc3ffce..928d7f3d7b 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1678,5 +1678,6 @@ define({ "PROMO_CARD_4_MESSAGE": "Edit headings, buttons, and copy directly in the preview.", "PROMO_LEARN_MORE": "Learn More\u2026", "PROMO_GET_APP_UPSELL_BUTTON": "Get {0}", - "PROMO_PRO_ENDED_TITLE": "Your {0} upgrade has ended" + "PROMO_PRO_ENDED_TITLE": "Your {0} upgrade has ended", + "PROMO_PRO_TRIAL_DAYS_LEFT": "Phoenix Pro Trial ({0} days left)" }); diff --git a/src/services/html/login-popup.html b/src/services/html/login-popup.html index 0e0de2569b..435d33bf48 100644 --- a/src/services/html/login-popup.html +++ b/src/services/html/login-popup.html @@ -1,6 +1,14 @@
{{/trialInfo}} + + {{Strings.GET_PHOENIX_PRO}} + +
diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index 947a661ae5..acf9994cdd 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -151,7 +151,10 @@ define(function (require, exports, module) { closePopup(); // close any existing popup first // Render template with basic data first for instant response - const renderedTemplate = Mustache.render(loginTemplate, {Strings}); + const renderedTemplate = Mustache.render(loginTemplate, { + Strings, + getProLink: brackets.config.purchase_url + }); $popup = $(renderedTemplate); $("body").append($popup); @@ -352,8 +355,9 @@ define(function (require, exports, module) { if (!$popup || !entitlements) { return; } - + // entitlements will always be present for login popup. // Update plan information + const $getProLink = $popup.find('.get-phoenix-pro-profile'); if (entitlements.plan) { const $planName = $popup.find('.user-plan-name'); @@ -371,6 +375,7 @@ define(function (require, exports, module) { `; $planName.addClass('user-plan-paid').html(proTitle); + $getProLink.removeClass('forced-hidden'); } else { // For paid users: regular plan name with icon const proTitle = ` @@ -378,11 +383,14 @@ define(function (require, exports, module) { `; $planName.addClass('user-plan-paid').html(proTitle); + $getProLink.addClass('forced-hidden'); } } else { // Use simple text for free users $planName.addClass('user-plan-free').text(entitlements.plan.name); } + } else { + $getProLink.removeClass('forced-hidden'); } // Update quota section if available @@ -432,7 +440,8 @@ define(function (require, exports, module) { titleText: "Ai Quota Used", usageText: "100 / 200 credits", usedPercent: 0, - Strings: Strings + Strings: Strings, + getProLink: brackets.config.purchase_url }; // Note: We don't await here to keep popup display instant diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less index fac74455ec..5dd433c43a 100644 --- a/src/styles/brackets_core_ui_variables.less +++ b/src/styles/brackets_core_ui_variables.less @@ -300,4 +300,4 @@ ); @phoenixPro-brand-light: #cc5500; -@phoenixPro-brand-dark: #ff8c42; +@phoenixPro-brand-dark: #FD8C2F; From 450958d7ae03c8175d8cc09e6473310bd399ade5 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 9 Sep 2025 14:03:46 +0530 Subject: [PATCH 06/10] chore: add html menu item support in CommandManager.register and 'Get Phoenix pro' in help --- src/command/CommandManager.js | 21 +++++++++++++++++++-- src/command/Commands.js | 3 +++ src/command/DefaultMenus.js | 2 ++ src/command/Menus.js | 5 +++++ src/help/HelpCommandHandlers.js | 5 +++++ src/nls/root/strings.js | 1 + 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/command/CommandManager.js b/src/command/CommandManager.js index 2fec64489e..6ad18f6053 100644 --- a/src/command/CommandManager.js +++ b/src/command/CommandManager.js @@ -148,6 +148,15 @@ define(function (require, exports, module) { return this._enabled; }; + /** + * get the command options + * + * @return {object} + */ + Command.prototype.getOptions = function () { + return this._options || {}; + }; + /** * Sets enabled state of Command and dispatches "enabledStateChange" * when the enabled state changes. @@ -196,11 +205,17 @@ define(function (require, exports, module) { * use \uXXXX instead of an HTML entity. * * @param {string} name + * @param {string} htmlName If set, this will be displayed in ui menus instead of the name given. + * Eg. "Phoenix menu" */ - Command.prototype.setName = function (name) { - var changed = this._name !== name; + Command.prototype.setName = function (name, htmlName) { + let changed = this._name !== name; this._name = name; + if (htmlName && this._options.htmlName !== htmlName) { + changed = true; + this._options.htmlName = htmlName; + } if (changed) { this.trigger("nameChange"); } @@ -233,6 +248,8 @@ define(function (require, exports, module) { * @param {boolean} options.eventSource If set to true, the commandFn will be called with the first argument `event` * with details about the source(invoker) as event.eventSource(one of the `CommandManager.SOURCE_*`) and * event.sourceType(Eg. Ctrl-K) parameter. + * @param {string} options.htmlName If set, this will be displayed in ui menus instead of the name given. + * Eg. "Phoenix menu" * @return {?Command} */ function register(name, id, commandFn, options={}) { diff --git a/src/command/Commands.js b/src/command/Commands.js index d94c550fd2..391f3e1d46 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -405,6 +405,9 @@ define(function (require, exports, module) { /** Opens support resources */ exports.HELP_SUPPORT = "help.support"; // HelpCommandHandlers.js _handleLinkMenuItem() + /** Opens Phoenix Pro page */ + exports.HELP_GET_PRO = "help.getPro"; // HelpCommandHandlers.js _handleLinkMenuItem() + /** Opens feature suggestion page */ exports.HELP_SUGGEST = "help.suggest"; // HelpCommandHandlers.js _handleLinkMenuItem() diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index e62c756d2e..e047b9886d 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -268,6 +268,8 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.HELP_DOCS); menu.addMenuItem(Commands.HELP_SUPPORT); menu.addMenuDivider(); + menu.addMenuItem(Commands.HELP_GET_PRO); + menu.addMenuDivider(); if (brackets.config.suggest_feature_url) { menu.addMenuItem(Commands.HELP_SUGGEST); } diff --git a/src/command/Menus.js b/src/command/Menus.js index 3bb133d192..2a221f053f 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -1108,6 +1108,11 @@ define(function (require, exports, module) { } }); } else { + const htmlName = this._command.getOptions().htmlName; + if(htmlName) { + $(_getHTMLMenuItem(this.id)).find(".menu-name").html(htmlName); + return; + } $(_getHTMLMenuItem(this.id)).find(".menu-name").text(this._command.getName()); } }; diff --git a/src/help/HelpCommandHandlers.js b/src/help/HelpCommandHandlers.js index b04ca7ea57..9fc47cc703 100644 --- a/src/help/HelpCommandHandlers.js +++ b/src/help/HelpCommandHandlers.js @@ -160,9 +160,14 @@ define(function (require, exports, module) { }); }); + const getProString = `${Strings.CMD_GET_PRO}`; + CommandManager.register(Strings.CMD_HOW_TO_USE_BRACKETS, Commands.HELP_HOW_TO_USE_BRACKETS, _handleLinkMenuItem(brackets.config.how_to_use_url)); CommandManager.register(Strings.CMD_DOCS, Commands.HELP_DOCS, _handleLinkMenuItem(brackets.config.docs_url)); CommandManager.register(Strings.CMD_SUPPORT, Commands.HELP_SUPPORT, _handleLinkMenuItem(brackets.config.support_url)); + CommandManager.register(Strings.CMD_GET_PRO, Commands.HELP_GET_PRO, _handleLinkMenuItem(brackets.config.purchase_url), { + htmlName: getProString + }); CommandManager.register(Strings.CMD_SUGGEST, Commands.HELP_SUGGEST, _handleLinkMenuItem(brackets.config.suggest_feature_url)); CommandManager.register(Strings.CMD_REPORT_ISSUE, Commands.HELP_REPORT_ISSUE, _handleLinkMenuItem(brackets.config.report_issue_url)); CommandManager.register(Strings.CMD_RELEASE_NOTES, Commands.HELP_RELEASE_NOTES, _handleLinkMenuItem(brackets.config.release_notes_url)); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 718c12b416..0f174feaeb 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -649,6 +649,7 @@ define({ "CMD_AUTO_UPDATE": "Auto Update", "CMD_HOW_TO_USE_BRACKETS": "How to Use {APP_NAME}", "CMD_SUPPORT": "{APP_NAME} Support", + "CMD_GET_PRO": "Get Phoenix Pro", "CMD_USER_PROFILE": "{APP_NAME} Account", "CMD_DOCS": "Help, Getting Started", "CMD_SUGGEST": "Suggest a Feature", From 50edc4c108e577ccf26681bc517812ad464637ff Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 9 Sep 2025 14:16:24 +0530 Subject: [PATCH 07/10] docs: update commandManager docs for htmlName field --- docs/API-Reference/command/CommandManager.md | 19 ++++++++++++++----- docs/API-Reference/command/Commands.md | 6 ++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/API-Reference/command/CommandManager.md b/docs/API-Reference/command/CommandManager.md index b2537b13af..4ff9b34e84 100644 --- a/docs/API-Reference/command/CommandManager.md +++ b/docs/API-Reference/command/CommandManager.md @@ -13,10 +13,11 @@ const CommandManager = brackets.getModule("command/CommandManager") * [.getID()](#Command+getID) ⇒ string * [.execute()](#Command+execute) ⇒ $.Promise * [.getEnabled()](#Command+getEnabled) ⇒ boolean + * [.getOptions()](#Command+getOptions) ⇒ object * [.setEnabled(enabled)](#Command+setEnabled) * [.setChecked(checked)](#Command+setChecked) * [.getChecked()](#Command+getChecked) ⇒ boolean - * [.setName(name)](#Command+setName) + * [.setName(name, htmlName)](#Command+setName) * [.getName()](#Command+getName) ⇒ string @@ -54,6 +55,12 @@ Executes the command. Additional arguments are passed to the executing function ### command.getEnabled() ⇒ boolean Is command enabled? +**Kind**: instance method of [Command](#Command) + + +### command.getOptions() ⇒ object +get the command options + **Kind**: instance method of [Command](#Command) @@ -87,7 +94,7 @@ Is command checked? **Kind**: instance method of [Command](#Command) -### command.setName(name) +### command.setName(name, htmlName) Sets the name of the Command and dispatches "nameChange" so that UI that reflects the command name can update. @@ -97,9 +104,10 @@ use \uXXXX instead of an HTML entity. **Kind**: instance method of [Command](#Command) -| Param | Type | -| --- | --- | -| name | string | +| Param | Type | Description | +| --- | --- | --- | +| name | string | | +| htmlName | string | If set, this will be displayed in ui menus instead of the name given. Eg. "Phoenix menu" | @@ -156,6 +164,7 @@ Registers a global command. | commandFn | function | the function to call when the command is executed. Any arguments passed to execute() (after the id) are passed as arguments to the function. If the function is asynchronous, it must return a jQuery promise that is resolved when the command completes. Otherwise, the CommandManager will assume it is synchronous, and return a promise that is already resolved. | | [options] | Object | | | options.eventSource | boolean | If set to true, the commandFn will be called with the first argument `event` with details about the source(invoker) as event.eventSource(one of the `CommandManager.SOURCE_*`) and event.sourceType(Eg. Ctrl-K) parameter. | +| options.htmlName | string | If set, this will be displayed in ui menus instead of the name given. Eg. "Phoenix menu" | diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index 783376af39..01c1fe59ca 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -734,6 +734,12 @@ Opens documentation ## HELP\_SUPPORT Opens support resources +**Kind**: global variable + + +## HELP\_GET\_PRO +Opens Phoenix Pro page + **Kind**: global variable From 98aff763458359a21e16fc93a3e8e5ad70964d5e Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 9 Sep 2025 15:20:32 +0530 Subject: [PATCH 08/10] test: unit and integ test for html menu item and command name --- src/command/CommandManager.js | 2 +- test/spec/CommandManager-test.js | 110 +++++++++++++++++++++++++++++++ test/spec/Menu-integ-test.js | 79 ++++++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) diff --git a/src/command/CommandManager.js b/src/command/CommandManager.js index 6ad18f6053..10b83d0bdc 100644 --- a/src/command/CommandManager.js +++ b/src/command/CommandManager.js @@ -212,7 +212,7 @@ define(function (require, exports, module) { let changed = this._name !== name; this._name = name; - if (htmlName && this._options.htmlName !== htmlName) { + if (this._options.htmlName !== htmlName) { changed = true; this._options.htmlName = htmlName; } diff --git a/test/spec/CommandManager-test.js b/test/spec/CommandManager-test.js index 521ae51b17..bd063fddf0 100644 --- a/test/spec/CommandManager-test.js +++ b/test/spec/CommandManager-test.js @@ -173,5 +173,115 @@ define(function (require, exports, module) { CommandManager.execute(commandID); expect(receivedEvent).not.toBeDefined(); }); + + it("register command with htmlName option", function () { + var htmlName = "Phoenix menu"; + var command = CommandManager.register("test command", "test-htmlname-command", testCommandFn, { + htmlName: htmlName + }); + expect(command).toBeTruthy(); + expect(command.getName()).toBe("test command"); + expect(command.getOptions().htmlName).toBe(htmlName); + }); + + it("getOptions should return empty object when no options provided", function () { + var command = CommandManager.register("test command", "test-no-options-command", testCommandFn); + expect(command).toBeTruthy(); + expect(command.getOptions()).toEql({}); + }); + + it("getOptions should return options when provided", function () { + var options = { + eventSource: true, + htmlName: "Test HTML Name" + }; + var command = CommandManager.register("test command", "test-with-options-command", testCommandFn, options); + expect(command).toBeTruthy(); + expect(command.getOptions()).toEql(options); + }); + + it("setName with htmlName parameter and trigger nameChange", function () { + var eventTriggered = false; + var command = CommandManager.register("test command", "test-setname-html-command", testCommandFn); + command.on("nameChange", function () { + eventTriggered = true; + }); + + var newName = "new command name"; + var htmlName = "New Name"; + command.setName(newName, htmlName); + + expect(eventTriggered).toBeTruthy(); + expect(command.getName()).toBe(newName); + expect(command.getOptions().htmlName).toBe(htmlName); + }); + + it("setName should trigger nameChange when only htmlName changes", function () { + var eventTriggered = false; + var command = CommandManager.register("test command", "test-setname-htmlonly-command", testCommandFn, { + htmlName: "original html" + }); + command.on("nameChange", function () { + eventTriggered = true; + }); + + var newHtmlName = "Updated HTML Name"; + command.setName(command.getName(), newHtmlName); + + expect(eventTriggered).toBeTruthy(); + expect(command.getOptions().htmlName).toBe(newHtmlName); + }); + + it("setName should not trigger nameChange when name and htmlName are unchanged", function () { + var eventTriggered = false; + var htmlName = "Same HTML Name"; + var command = CommandManager.register("test command", "test-setname-same-command", testCommandFn, { + htmlName: htmlName + }); + command.on("nameChange", function () { + eventTriggered = true; + }); + + command.setName(command.getName(), htmlName); + + expect(eventTriggered).toBeFalsy(); + }); + + it("should handle edge cases for htmlName", function () { + // Test with empty string htmlName + var command1 = CommandManager.register("test command", "test-empty-html-command", testCommandFn, { + htmlName: "" + }); + expect(command1.getOptions().htmlName).toBe(""); + + // Test with null htmlName + var command2 = CommandManager.register("test command", "test-null-html-command", testCommandFn, { + htmlName: null + }); + expect(command2.getOptions().htmlName).toBe(null); + + // Test with undefined htmlName (should not be set) + var command3 = CommandManager.register("test command", "test-undefined-html-command", testCommandFn, { + htmlName: undefined + }); + expect(command3.getOptions().htmlName).toBe(undefined); + }); + + it("setName should handle edge cases for htmlName parameter", function () { + var command = CommandManager.register("test command", "test-setname-edge-command", testCommandFn); + + // Test setting htmlName to empty string + command.setName("test name", ""); + expect(command.getOptions().htmlName).toBe(""); + + // Test setting htmlName to null (should still trigger change if it was different) + var eventTriggered = false; + command.on("nameChange", function () { + eventTriggered = true; + }); + command.setName("test name", null); + expect(command.getOptions().htmlName).toBe(null); + expect(eventTriggered).toBeTruthy(); + }); }); }); diff --git a/test/spec/Menu-integ-test.js b/test/spec/Menu-integ-test.js index b79671ffb2..fdd5140909 100644 --- a/test/spec/Menu-integ-test.js +++ b/test/spec/Menu-integ-test.js @@ -403,6 +403,85 @@ define(function (require, exports, module) { hideCommand.setEnabled(true); expect(element.getElementsByClassName("forced-hidden").length).toBe(0); }); + + it("should display htmlName in menu item when provided", async function () { + const utMenuID = "menuitem-htmlname-test"; + const testCmd = "Menu-test.htmlname-command"; + const htmlName = "Phoenix menu"; + + // Register command with htmlName option + CommandManager.register("Plain Text Name", testCmd, function () {}, { + htmlName: htmlName + }); + + const menu = Menus.addMenu("HTML Name Test Menu", utMenuID); + const menuItem = menu.addMenuItem(testCmd); + expect(menuItem).toBeTruthy(); + + const listSelector = "#menuitem-htmlname-test > ul"; + const $listItems = testWindow.$(listSelector).children(); + expect($listItems.length).toBe(1); + + // Check that the menu item contains the HTML content + const $menuLink = $($listItems[0]).find("a .menu-name"); + expect($menuLink.html().includes("Phoenix menu")).toBeTrue(); + expect($menuLink.html().includes("fa fa-car")).toBeTrue(); + expect($menuLink.html().includes("margin-left: 4px;")).toBeTrue(); + + // Verify the HTML is rendered (not just as text) + expect($menuLink.find("i.fa.fa-car").length).toBe(1); + }); + + it("should fall back to regular name when htmlName is not provided", async function () { + const utMenuID = "menuitem-fallback-test"; + const testCmd = "Menu-test.fallback-command"; + const plainName = "Plain Menu Name"; + + // Register command without htmlName option + CommandManager.register(plainName, testCmd, function () {}); + + const menu = Menus.addMenu("Fallback Test Menu", utMenuID); + const menuItem = menu.addMenuItem(testCmd); + expect(menuItem).toBeTruthy(); + + const listSelector = "#menuitem-fallback-test > ul"; + const $listItems = testWindow.$(listSelector).children(); + expect($listItems.length).toBe(1); + + // Check that the menu item displays the regular name (no HTML) + const $menuLink = $($listItems[0]).find("a .menu-name"); + expect($menuLink.text()).toBe(plainName); + }); + + it("should update menu display when htmlName is changed via setName", async function () { + const utMenuID = "menuitem-setname-test"; + const testCmd = "Menu-test.setname-command"; + const originalName = "Original Name"; + const newName = "Updated Name"; + const newHtmlName = "Updated Bold Name"; + + // Register command without htmlName initially + const command = CommandManager.register(originalName, testCmd, function () {}); + + const menu = Menus.addMenu("SetName Test Menu", utMenuID); + const menuItem = menu.addMenuItem(testCmd); + expect(menuItem).toBeTruthy(); + + const listSelector = "#menuitem-setname-test > ul"; + const $listItems = testWindow.$(listSelector).children(); + const $menuLink = $($listItems[0]).find("a .menu-name"); + + // Initially should show regular text name + expect($menuLink.text()).toBe(originalName); + + // Update name with htmlName + command.setName(newName, newHtmlName); + + // Should now display HTML content + expect($menuLink.html()).toBe(newHtmlName); + expect($menuLink.find("b").length).toBe(1); + expect($menuLink.find("b").text()).toBe("Bold"); + }); }); From 291fc7791dc331f7f78622239855f993040fc663 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 9 Sep 2025 16:44:55 +0530 Subject: [PATCH 09/10] chore: update docs for LoginService.getEffectiveEntitlements, refactor brackets.config.main_pro_plan --- src/config.json | 1 + src/services/login-service.js | 114 ++++++++++++++++++++++++++++++++-- src/services/pro-dialogs.js | 4 +- src/services/profile-menu.js | 4 +- 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/config.json b/src/config.json index 3bba88b870..393c4010fd 100644 --- a/src/config.json +++ b/src/config.json @@ -2,6 +2,7 @@ "config": { "app_title": "Phoenix Code", "app_name_about": "Phoenix Code", + "main_pro_plan": "Phoenix Pro", "about_icon": "styles/images/phoenix-icon.svg", "account_url": "https://account.phcode.dev/", "promotions_url": "https://promotions.phcode.dev/dev/", diff --git a/src/services/login-service.js b/src/services/login-service.js index f2e3dac4c6..83c157b18d 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -27,6 +27,8 @@ define(function (require, exports, module) { require("./setup-login-service"); // this adds loginService to KernalModeTrust require("./promotions"); + const MS_IN_DAY = 10 * 24 * 60 * 60 * 1000; + const KernalModeTrust = window.KernalModeTrust; if(!KernalModeTrust){ // integrated extensions will have access to kernal mode, but not external extensions @@ -123,8 +125,91 @@ define(function (require, exports, module) { } /** - * Get effective entitlements including trial enhancement for UI display - * Returns enhanced entitlements that treat trial users as pro users + * 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. + * + * @returns {Promise} Entitlements object or null if not logged in and no trial active + * + * @description Response shapes vary based on user state: + * + * **For non-logged-in users:** + * - Returns `null` if no trial is active + * - Returns synthetic entitlements if trial is active: + * ```javascript + * { + * plan: { + * paidSubscriber: true, // Always true for trial users + * name: "Phoenix Pro" + * }, + * isInProTrial: true, // Indicates this is a trial user + * trialDaysRemaining: number, // Days left in trial + * entitlements: { + * liveEdit: { + * activated: true // Trial users get liveEdit access + * } + * } + * } + * ``` + * + * **For logged-in trial users:** + * - If remote response has `plan.paidSubscriber: false`, injects `paidSubscriber: true` + * - Adds `isInProTrial: true` and `trialDaysRemaining` + * - Injects `entitlements.liveEdit.activated: true` + * - Note: Trial users may not be actual paid subscribers, but `paidSubscriber: true` is set + * so all Phoenix code treats them as paid subscribers + * + * **For logged-in users (full remote response):** + * ```javascript + * { + * isSuccess: boolean, + * lang: string, + * plan: { + * name: "Phoenix Pro", + * paidSubscriber: boolean, + * validTill: number // Timestamp + * }, + * profileview: { + * quota: { + * titleText: "Ai Quota Used", + * usageText: "100 / 200 credits", + * usedPercent: number + * }, + * htmlMessage: string // HTML alert message + * }, + * entitlements: { + * liveEdit: { + * activated: boolean, + * subscribeURL: string, // URL to subscribe if not activated + * upgradeToPlan: string, // Plan name that includes this entitlement + * validTill: number // Timestamp when entitlement expires + * }, + * liveEditAI: { + * activated: boolean, + * subscribeURL: string, + * purchaseCreditsURL: string, // URL to purchase AI credits + * upgradeToPlan: string, + * validTill: number + * } + * } + * } + * ``` + * + * @example + * // Listen for entitlements changes + * const LoginService = window.KernelModeTrust.loginService; + * LoginService.on(LoginService.EVENT_ENTITLEMENTS_CHANGED, (entitlements) => { + * console.log('Entitlements changed:', entitlements); + * // Update UI based on new entitlements + * }); + * + * // Get current entitlements + * const entitlements = await LoginService.getEffectiveEntitlements(); + * if (entitlements?.plan?.paidSubscriber) { + * // Enable pro features + * } + * if (entitlements?.entitlements?.liveEdit?.activated) { + * // Enable live edit feature + * } */ async function getEffectiveEntitlements(forceRefresh = false) { // Get raw server entitlements @@ -151,10 +236,19 @@ define(function (require, exports, module) { plan: { ...serverEntitlements.plan, paidSubscriber: true, - name: "Phoenix Pro" + name: brackets.config.main_pro_plan }, isInProTrial: true, - trialDaysRemaining: trialDaysRemaining + 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 { @@ -162,10 +256,18 @@ define(function (require, exports, module) { return { plan: { paidSubscriber: true, - name: "Phoenix Pro" + name: brackets.config.main_pro_plan }, isInProTrial: true, - trialDaysRemaining: trialDaysRemaining + trialDaysRemaining: trialDaysRemaining, + entitlements: { + liveEdit: { + activated: true, + subscribeURL: brackets.config.purchase_url, + upgradeToPlan: brackets.config.main_pro_plan, + validTill: Date.now() + trialDaysRemaining * MS_IN_DAY + } + } }; } } diff --git a/src/services/pro-dialogs.js b/src/services/pro-dialogs.js index 6dbbf6cb38..236ec5a1ba 100644 --- a/src/services/pro-dialogs.js +++ b/src/services/pro-dialogs.js @@ -27,10 +27,10 @@ define(function (require, exports, module) { const proTitle = ` - Phoenix Pro + ${brackets.config.main_pro_plan} `, - proTitlePlain = `Phoenix Pro + proTitlePlain = `${brackets.config.main_pro_plan} `; require("./setup-login-service"); // this adds loginService to KernalModeTrust const Dialogs = require("widgets/Dialogs"), diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index acf9994cdd..c582234f0c 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -230,9 +230,9 @@ define(function (require, exports, module) { } if (entitlements && entitlements.plan && entitlements.plan.paidSubscriber) { // Pro user (paid subscriber or trial): show plan name with feather icon - let displayName = entitlements.plan.name || "Phoenix Pro"; + let displayName = entitlements.plan.name || brackets.config.main_pro_plan; if (entitlements.isInProTrial) { - displayName = `Phoenix Pro`; // Just "Phoenix Pro" for branding, not "Phoenix Pro Trial" + displayName = brackets.config.main_pro_plan; // Just "Phoenix Pro" for branding, not "Phoenix Pro Trial" } $brandingLink .attr("href", "https://account.phcode.dev") From 021a026629c72179e176fe623b4bc38a69a6f715 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 9 Sep 2025 19:38:25 +0530 Subject: [PATCH 10/10] fix: windows desktop eslint integ tests fails due to timouts --- test/spec/Extn-ESLint-integ-test.js | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/spec/Extn-ESLint-integ-test.js b/test/spec/Extn-ESLint-integ-test.js index b91bf827fe..5a07682304 100644 --- a/test/spec/Extn-ESLint-integ-test.js +++ b/test/spec/Extn-ESLint-integ-test.js @@ -87,7 +87,7 @@ define(function (require, exports, module) { async function _waitForProblemsPanelVisible(visible) { await awaitsFor(()=>{ return $("#problems-panel").is(":visible") === visible; - }, "Problems panel to be visible"); + }, "Problems panel to be visible", 15000); } async function _openSimpleES6Project() { @@ -160,12 +160,12 @@ define(function (require, exports, module) { await _waitForProblemsPanelVisible(true); await awaitsFor(()=>{ return $("#problems-panel").text().includes(Strings.DESCRIPTION_ESLINT_LOAD_FAILED); - }, "ESLint v6 not supported error to be shown"); + }, "ESLint v6 not supported error to be shown", 15000); } it("should ESLint v6 show unsupported version error", async function () { await _loadAndValidateES6Project(); - }, 5000); + }, 30000); it("should show ESLint and JSHint in desktop app for es6 project or below", async function () { await _loadAndValidateES6Project(); @@ -173,7 +173,7 @@ define(function (require, exports, module) { await _fileSwitcherroForESLintFailDetection(); return $("#problems-panel").text().includes(JSHintErrorES6Error_js); }, "JShint error to be shown", 3000, 300); - }, 5000); + }, 30000); }); describe("ES7 and JSHint project", function () { @@ -190,20 +190,20 @@ define(function (require, exports, module) { await _waitForProblemsPanelVisible(true); await awaitsFor(()=>{ return $("#problems-panel").text().includes(ESLintErrorES7Error_js); - }, "ESLint v7 error to be shown"); + }, "ESLint v7 error to be shown", 15000); } it("should ESLint v7 work as expected", async function () { await _loadAndValidateES7Project(); - }, 5000); + }, 30000); it("should show ESLint and JSHint in desktop app if .jshintrc Present", async function () { await _loadAndValidateES7Project(); await awaitsFor(()=>{ return $("#problems-panel").text().includes(JSHintErrorES6Error_js); - }, "JShint error to be shown"); + }, "JShint error to be shown", 10000); expect($("#problems-panel").text().includes("JSHint")).toBeTrue(); - }, 5000); + }, 30000); }); describe("ES8 with react js support project", function () { // this should cover es7 too @@ -220,8 +220,8 @@ define(function (require, exports, module) { await _waitForProblemsPanelVisible(true); await awaitsFor(()=>{ return $("#problems-panel").text().includes(ESLintReactError_js); - }, "ESLint jsx error to be shown"); - }, 5000); + }, "ESLint jsx error to be shown", 15000); + }, 30000); }); // we should have an es9 test too as above, but es9 currently doesnt support jsx @@ -241,25 +241,25 @@ define(function (require, exports, module) { await _waitForProblemsPanelVisible(true); await awaitsFor(()=>{ return $("#problems-panel").text().includes(ESLintErrorES8Error_js); - }, "ESLint v8 error to be shown"); + }, "ESLint v8 error to be shown", 15000); } it("should ESLint v8 work as expected", async function () { await _loadAndValidateES8Project(); - }, 5000); + }, 30000); it("should not lint jsx file as ESLint v8 is not configured for react lint", async function () { await _openProjectFile("react.jsx"); await awaits(100); // Just wait for some time to prevent any false linter runs await _waitForProblemsPanelVisible(false); expect($("#status-inspection").hasClass("inspection-disabled")).toBeTrue(); - }, 5000); + }, 30000); it("should not show JSHint in desktop app if ESLint is active", async function () { await _loadAndValidateES8Project(); await awaits(100); // give some time so that jshint has time to complete if there is any. expect($("#problems-panel").text().includes("JSHint")).toBeFalse(); - }, 5000); + }, 30000); }); describe("ES Latest module project", function () { @@ -373,7 +373,7 @@ define(function (require, exports, module) { await _waitForProblemsPanelVisible(true); await awaitsFor(()=>{ return $("#problems-panel").find(".ph-fix-problem").length === 2; - }, "There should be 2 fix problem button in the panel"); + }, "There should be 2 fix problem button in the panel", 15000); } async function _triggerLint() { @@ -383,7 +383,7 @@ define(function (require, exports, module) { it("should ESLint v9 show fix buttons", async function () { await _openAndVerifyInitial(); - }, 5000); + }, 30000); it("should be able to fix 1 error", async function () { await _openAndVerifyInitial(); @@ -391,7 +391,7 @@ define(function (require, exports, module) { $($("#problems-panel").find(".ph-fix-problem")[0]).click(); await awaitsFor(()=>{ return $("#problems-panel").find(".ph-fix-problem").length === 1; - }, "only 1 problem should remain"); + }, "only 1 problem should remain", 15000); // it should select the edited text const editor = EditorManager.getCurrentFullEditor(); @@ -406,8 +406,8 @@ define(function (require, exports, module) { await _triggerLint(); await awaitsFor(()=>{ return $("#problems-panel").find(".ph-fix-problem").length === 2; - }, "2 problem should be there"); - }, 5000); + }, "2 problem should be there", 15000); + }, 30000); it("should be able to fix all errors", async function () { await _openAndVerifyInitial(); @@ -417,7 +417,7 @@ define(function (require, exports, module) { $($("#problems-panel").find(".problems-fix-all-btn")).click(); await awaitsFor(()=>{ return $("#problems-panel").find(".ph-fix-problem").length === 0; - }, "no problems should remain as all is now fixed"); + }, "no problems should remain as all is now fixed", 15000); // fixing multiple should place the cursor on first fix expect(editor.hasSelection()).toBeFalse(); @@ -430,8 +430,8 @@ define(function (require, exports, module) { await _triggerLint(); await awaitsFor(()=>{ return $("#problems-panel").find(".ph-fix-problem").length === 2; - }, "2 problem should be there"); - }, 5000); + }, "2 problem should be there", 15000); + }, 30000); }); }); });