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 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/command/CommandManager.js b/src/command/CommandManager.js index 2fec64489e..10b83d0bdc 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 (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/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/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 d9efc3ffce..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", @@ -1678,5 +1679,7 @@ 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} Trial has ended", + "PROMO_PRO_TRIAL_DAYS_LEFT": "Phoenix Pro Trial ({0} days left)", + "GET_PHOENIX_PRO": "Get Phoenix Pro" }); diff --git a/src/services/html/login-popup.html b/src/services/html/login-popup.html index 0e0de2569b..fafc0c00e2 100644 --- a/src/services/html/login-popup.html +++ b/src/services/html/login-popup.html @@ -1,6 +1,18 @@
diff --git a/src/services/login-browser.js b/src/services/login-browser.js index 8e0dd953cf..e6b2351992 100644 --- a/src/services/login-browser.js +++ b/src/services/login-browser.js @@ -393,7 +393,6 @@ define(function (require, exports, module) { // Only set exports for browser apps to avoid conflict with desktop login if (!Phoenix.isNativeApp) { - init(); // kernal exports // Add to existing KernalModeTrust.loginService from login-service.js LoginService.isLoggedIn = isLoggedIn; @@ -402,6 +401,7 @@ define(function (require, exports, module) { LoginService.getProfile = getProfile; LoginService.verifyLoginStatus = () => _verifyBrowserLogin(false); LoginService.getAccountBaseURL = _getAccountBaseURL; + init(); } // public exports diff --git a/src/services/login-desktop.js b/src/services/login-desktop.js index 8ead917b97..98a2969ab7 100644 --- a/src/services/login-desktop.js +++ b/src/services/login-desktop.js @@ -407,7 +407,6 @@ define(function (require, exports, module) { // Only set exports for native apps to avoid conflict with browser login if (Phoenix.isNativeApp) { - init(); // kernal exports - add to existing KernalModeTrust.loginService from login-service.js LoginService.isLoggedIn = isLoggedIn; LoginService.signInToAccount = signInToAccount; @@ -415,6 +414,7 @@ define(function (require, exports, module) { LoginService.getProfile = getProfile; LoginService.verifyLoginStatus = () => _verifyLogin(false); LoginService.getAccountBaseURL = getAccountBaseURL; + init(); } // public exports diff --git a/src/services/login-service.js b/src/services/login-service.js index 86256b97a4..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 @@ -122,8 +124,157 @@ define(function (require, exports, module) { } } + /** + * 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 + const serverEntitlements = await getEntitlements(forceRefresh); + + // Get trial days remaining + const trialDaysRemaining = await LoginService.getProTrialDaysRemaining(); + + // If no trial is active, return server entitlements as-is + if (trialDaysRemaining <= 0) { + return serverEntitlements; + } + + // User has active trial + if (serverEntitlements && serverEntitlements.plan) { + // Logged-in user with trial + if (serverEntitlements.plan.paidSubscriber) { + // Already a paid subscriber, return as-is + return serverEntitlements; + } else { + // Enhance entitlements for trial user + return { + ...serverEntitlements, + plan: { + ...serverEntitlements.plan, + paidSubscriber: true, + name: brackets.config.main_pro_plan + }, + isInProTrial: true, + 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 { + // Non-logged-in user with trial - return synthetic entitlements + return { + plan: { + paidSubscriber: true, + name: brackets.config.main_pro_plan + }, + isInProTrial: true, + trialDaysRemaining: trialDaysRemaining, + entitlements: { + liveEdit: { + activated: true, + subscribeURL: brackets.config.purchase_url, + upgradeToPlan: brackets.config.main_pro_plan, + validTill: Date.now() + trialDaysRemaining * MS_IN_DAY + } + } + }; + } + } + // Add functions to secure exports LoginService.getEntitlements = getEntitlements; + LoginService.getEffectiveEntitlements = getEffectiveEntitlements; LoginService.clearEntitlements = clearEntitlements; LoginService.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED; }); 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 89661ec641..c582234f0c 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -3,6 +3,7 @@ define(function (require, exports, module) { PopUpManager = require("widgets/PopUpManager"), ThemeManager = require("view/ThemeManager"), Strings = require("strings"), + StringUtils = require("utils/StringUtils"), LoginService = require("./login-service"); const KernalModeTrust = window.KernalModeTrust; @@ -149,8 +150,11 @@ define(function (require, exports, module) { // create the popup element closePopup(); // close any existing popup first - // Render template with data - const renderedTemplate = Mustache.render(loginTemplate, {Strings}); + // Render template with basic data first for instant response + const renderedTemplate = Mustache.render(loginTemplate, { + Strings, + getProLink: brackets.config.purchase_url + }); $popup = $(renderedTemplate); $("body").append($popup); @@ -158,6 +162,25 @@ define(function (require, exports, module) { positionPopup(); + // Check for trial info asynchronously and update popup + KernalModeTrust.loginService.getEffectiveEntitlements().then(effectiveEntitlements => { + if (effectiveEntitlements && effectiveEntitlements.isInProTrial && isPopupVisible && $popup) { + // Add trial info to the existing popup + const planName = StringUtils.format(Strings.PROMO_PRO_TRIAL_DAYS_LEFT, + effectiveEntitlements.trialDaysRemaining); + const trialInfoHtml = `
+ + ${planName} + + +
`; + $popup.find('.popup-title').after(trialInfoHtml); + positionPopup(); // Reposition after adding content + } + }).catch(error => { + console.error('Failed to check trial info for login popup:', error); + }); + PopUpManager.addPopUp($popup, function() { $popup.remove(); $popup = null; @@ -206,12 +229,15 @@ define(function (require, exports, module) { }; } if (entitlements && entitlements.plan && entitlements.plan.paidSubscriber) { - // Paid subscriber: show plan name with feather icon - const planName = entitlements.plan.name || "Phoenix Pro"; + // Pro user (paid subscriber or trial): show plan name with feather icon + 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" + } $brandingLink .attr("href", "https://account.phcode.dev") .addClass("phoenix-pro") - .html(`${planName}`); + .html(`${displayName}`); } else { // Free user: show phcode.io branding $brandingLink @@ -329,16 +355,42 @@ 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'); - $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 pro users (paid or trial) + if (entitlements.isInProTrial) { + // For trial users: separate "Phoenix Pro" with icon from "(X days left)" text + const planName = StringUtils.format(Strings.PROMO_PRO_TRIAL_DAYS_LEFT, + entitlements.trialDaysRemaining); + const proTitle = ` + ${planName} + + `; + $planName.addClass('user-plan-paid').html(proTitle); + $getProLink.removeClass('forced-hidden'); + } else { + // For paid users: regular plan name with icon + const proTitle = ` + ${entitlements.plan.name} + + `; + $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 @@ -388,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 @@ -403,8 +456,8 @@ define(function (require, exports, module) { positionPopup(); - // Apply cached entitlements immediately if available (including quota/messages) - KernalModeTrust.loginService.getEntitlements(false).then(cachedEntitlements => { + // Apply cached effective entitlements immediately if available (including quota/messages) + KernalModeTrust.loginService.getEffectiveEntitlements(false).then(cachedEntitlements => { if (cachedEntitlements && isPopupVisible) { _updatePopupWithEntitlements(cachedEntitlements); } @@ -453,8 +506,7 @@ define(function (require, exports, module) { */ async function _refreshEntitlementsInBackground() { try { - // Fetch fresh entitlements from API - const freshEntitlements = await KernalModeTrust.loginService.getEntitlements(true); // Force refresh to get latest data + const freshEntitlements = await KernalModeTrust.loginService.getEffectiveEntitlements(true); // Only update popup if it's still visible if (isPopupVisible && $popup && freshEntitlements) { @@ -515,6 +567,36 @@ define(function (require, exports, module) { }); } + /** + * Check if user has an active trial (works for both logged-in and non-logged-in users) + */ + async function _hasActiveTrial() { + try { + const effectiveEntitlements = await KernalModeTrust.loginService.getEffectiveEntitlements(); + return effectiveEntitlements && effectiveEntitlements.isInProTrial; + } catch (error) { + console.error('Failed to check trial status:', error); + return false; + } + } + + /** + * Initialize branding for non-logged-in trial users on startup + */ + async function _initializeBrandingForTrialUsers() { + try { + const effectiveEntitlements = await KernalModeTrust.loginService.getEffectiveEntitlements(); + if (effectiveEntitlements && effectiveEntitlements.isInProTrial) { + console.log('Profile Menu: Found active trial, updating branding...'); + _updateBranding(effectiveEntitlements); + } else { + console.log('Profile Menu: No active trial found'); + } + } catch (error) { + console.error('Failed to initialize branding for trial users:', error); + } + } + function init() { const helpButtonID = "user-profile-button"; $icon = $("") @@ -528,6 +610,17 @@ define(function (require, exports, module) { $icon.on('click', ()=>{ togglePopup(); }); + + // Initialize branding for non-logged-in trial users + _initializeBrandingForTrialUsers(); + + // Listen for entitlements changes to update branding for non-logged-in trial users + KernalModeTrust.loginService.on(KernalModeTrust.loginService.EVENT_ENTITLEMENTS_CHANGED, () => { + // When entitlements change (including trial activation) for non-logged-in users, update branding + if (!KernalModeTrust.loginService.isLoggedIn()) { + _initializeBrandingForTrialUsers(); + } + }); } function setNotLoggedIn() { @@ -537,11 +630,24 @@ define(function (require, exports, module) { } _removeProfileIcon(); + // Reset branding, but preserve trial branding if user has active trial + _hasActiveTrial().then(hasActiveTrial => { + if (!hasActiveTrial) { + // Only reset branding if no trial exists + console.log('Profile Menu: No trial, resetting branding to free'); + _updateBranding(null); + } else { + // User has trial, maintain pro branding + console.log('Profile Menu: Trial exists, maintaining pro branding'); + _initializeBrandingForTrialUsers(); + } + }).catch(error => { + console.error('Failed to check trial status during logout:', error); + // Fallback to resetting branding + _updateBranding(null); + }); // Clear cached entitlements when user logs out KernalModeTrust.loginService.clearEntitlements(); - - // Reset branding to free mode - _updateBranding(null); } function setLoggedIn(initial, color) { @@ -551,11 +657,11 @@ define(function (require, exports, module) { } _updateProfileIcon(initial, color); - // Preload entitlements when user logs in - KernalModeTrust.loginService.getEntitlements() + // Preload effective entitlements when user logs in + KernalModeTrust.loginService.getEffectiveEntitlements() .then(_updateBranding) .catch(error => { - console.error('Failed to preload entitlements on login:', error); + console.error('Failed to preload effective entitlements on login:', error); }); } diff --git a/src/services/promotions.js b/src/services/promotions.js index 69a53c5b78..9efcc4735e 100644 --- a/src/services/promotions.js +++ b/src/services/promotions.js @@ -191,17 +191,16 @@ define(function (require, exports, module) { } /** - * Check if pro trial is currently activated + * Get remaining pro trial days + * Returns 0 if no trial or trial expired */ - async function isProTrialActivated() { + async function getProTrialDaysRemaining() { const trialData = await _getTrialData(); if (!trialData) { - return false; + return 0; } - const remainingDays = _calculateRemainingTrialDays(trialData); - - return remainingDays > 0; + return _calculateRemainingTrialDays(trialData); } async function activateProTrial() { @@ -290,6 +289,11 @@ define(function (require, exports, module) { trialDays: trialDays, isFirstInstall: !existingTrialData }); + + // Also trigger entitlements changed event since effective entitlements have changed + // This allows UI components to update based on the new trial status + const effectiveEntitlements = await LoginService.getEffectiveEntitlements(); + LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED, effectiveEntitlements); } function _isAnyDialogsVisible() { @@ -321,7 +325,7 @@ define(function (require, exports, module) { }, TRIAL_POLL_MS); // Add to secure exports - LoginService.isProTrialActivated = isProTrialActivated; + LoginService.getProTrialDaysRemaining = getProTrialDaysRemaining; LoginService.EVENT_PRO_UPGRADE_ON_INSTALL = EVENT_PRO_UPGRADE_ON_INSTALL; // no public exports to prevent extension tampering diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less index 8575b85431..5dd433c43a 100644 --- a/src/styles/brackets_core_ui_variables.less +++ b/src/styles/brackets_core_ui_variables.less @@ -298,3 +298,6 @@ #e6a600 75%, /* darker amber */ #c99700 100% /* rich golden brown */ ); + +@phoenixPro-brand-light: #cc5500; +@phoenixPro-brand-dark: #FD8C2F; diff --git a/src/styles/phoenix-pro.less b/src/styles/phoenix-pro.less index 86c803d41c..60aff4c879 100644 --- a/src/styles/phoenix-pro.less +++ b/src/styles/phoenix-pro.less @@ -21,6 +21,14 @@ } } +.phoenix-pro-title-plain { + color: @phoenixPro-brand-light; + + .dark & { + color: @phoenixPro-brand-dark; + } +} + /* Dialog styles with light default + .dark overrides */ .browser-login-waiting-dialog, .pro-upgrade-dialog{ /* ---- Layout ---- */ 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/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); }); }); }); 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"); + }); });