+
+
+
diff --git a/src/services/login-browser.js b/src/services/login-browser.js
index 8f0338dfba..be0c15fab0 100644
--- a/src/services/login-browser.js
+++ b/src/services/login-browser.js
@@ -51,6 +51,7 @@ define(function (require, exports, module) {
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
ProfileMenu = require("./profile-menu"),
+ LoginService = require("./login-service"),
Mustache = require("thirdparty/mustache/mustache"),
browserLoginWaitingTemplate = require("text!./html/browser-login-waiting-dialog.html");
@@ -418,6 +419,8 @@ define(function (require, exports, module) {
secureExports.getProfile = getProfile;
secureExports.verifyLoginStatus = () => _verifyBrowserLogin(false);
secureExports.getAccountBaseURL = _getAccountBaseURL;
+ secureExports.getEntitlements = LoginService.getEntitlements;
+ secureExports.EVENT_ENTITLEMENTS_CHANGED = LoginService.EVENT_ENTITLEMENTS_CHANGED;
}
// public exports
diff --git a/src/services/login-desktop.js b/src/services/login-desktop.js
index b960d369d3..163f0e25dc 100644
--- a/src/services/login-desktop.js
+++ b/src/services/login-desktop.js
@@ -27,6 +27,7 @@ define(function (require, exports, module) {
Strings = require("strings"),
NativeApp = require("utils/NativeApp"),
ProfileMenu = require("./profile-menu"),
+ LoginService = require("./login-service"),
Mustache = require("thirdparty/mustache/mustache"),
NodeConnector = require("NodeConnector"),
otpDialogTemplate = require("text!./html/otp-dialog.html");
@@ -417,6 +418,8 @@ define(function (require, exports, module) {
secureExports.getProfile = getProfile;
secureExports.verifyLoginStatus = () => _verifyLogin(false);
secureExports.getAccountBaseURL = getAccountBaseURL;
+ secureExports.getEntitlements = LoginService.getEntitlements;
+ secureExports.EVENT_ENTITLEMENTS_CHANGED = LoginService.EVENT_ENTITLEMENTS_CHANGED;
}
// public exports
diff --git a/src/services/login-service.js b/src/services/login-service.js
new file mode 100644
index 0000000000..96a1cf765a
--- /dev/null
+++ b/src/services/login-service.js
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ *
+ */
+
+/**
+ * Shared Login Service
+ *
+ * This module contains shared login service functionality used by both
+ * browser and desktop login implementations, including entitlements management.
+ */
+
+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("Login service should have access to KernalModeTrust. Cannot boot without trust ring");
+ }
+
+ // Event constants
+ const EVENT_ENTITLEMENTS_CHANGED = "entitlements_changed";
+
+ // Cached entitlements data
+ let cachedEntitlements = null;
+
+ /**
+ * Get entitlements from API or cache
+ * Returns null if user is not logged in
+ */
+ async function getEntitlements(forceRefresh = false) {
+ // Return null if not logged in
+ if (!KernalModeTrust.loginService.isLoggedIn()) {
+ return null;
+ }
+
+ // Return cached data if available and not forcing refresh
+ if (cachedEntitlements && !forceRefresh) {
+ return cachedEntitlements;
+ }
+
+ try {
+ const accountBaseURL = KernalModeTrust.loginService.getAccountBaseURL();
+ const language = Phoenix.app && Phoenix.app.language ? Phoenix.app.language : 'en';
+ let url = `${accountBaseURL}/getAppEntitlements?lang=${language}`;
+ let fetchOptions = {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ };
+
+ // Handle different authentication methods for browser vs desktop
+ if (Phoenix.isNativeApp) {
+ // Desktop app: use appSessionID and validationCode
+ const profile = KernalModeTrust.loginService.getProfile();
+ if (profile && profile.apiKey && profile.validationCode) {
+ url += `&appSessionID=${encodeURIComponent(profile.apiKey)}&validationCode=${encodeURIComponent(profile.validationCode)}`;
+ } else {
+ console.error('Missing appSessionID or validationCode for desktop app entitlements');
+ return null;
+ }
+ } else {
+ // Browser app: use session cookies
+ fetchOptions.credentials = 'include';
+ }
+
+ const response = await fetch(url, fetchOptions);
+
+ if (response.ok) {
+ const result = await response.json();
+ if (result.isSuccess) {
+ // Check if entitlements actually changed
+ const entitlementsChanged = JSON.stringify(cachedEntitlements) !== JSON.stringify(result);
+
+ cachedEntitlements = result;
+
+ // Trigger event if entitlements changed
+ if (entitlementsChanged) {
+ KernalModeTrust.loginService.trigger(EVENT_ENTITLEMENTS_CHANGED, result);
+ }
+
+ return cachedEntitlements;
+ }
+ }
+ } catch (error) {
+ console.error('Failed to fetch entitlements:', error);
+ }
+
+ return null;
+ }
+
+ /**
+ * Clear cached entitlements and trigger change event
+ * Called when user logs out
+ */
+ function clearEntitlements() {
+ if (cachedEntitlements) {
+ cachedEntitlements = null;
+
+ // Trigger event when entitlements are cleared
+ if (KernalModeTrust.loginService.trigger) {
+ KernalModeTrust.loginService.trigger(EVENT_ENTITLEMENTS_CHANGED, null);
+ }
+ }
+ }
+
+ // Exports
+ exports.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED;
+ exports.getEntitlements = getEntitlements;
+ exports.clearEntitlements = clearEntitlements;
+});
diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js
index 8da38fcfb1..7390539a11 100644
--- a/src/services/profile-menu.js
+++ b/src/services/profile-menu.js
@@ -2,7 +2,8 @@ define(function (require, exports, module) {
const Mustache = require("thirdparty/mustache/mustache"),
PopUpManager = require("widgets/PopUpManager"),
ThemeManager = require("view/ThemeManager"),
- Strings = require("strings");
+ Strings = require("strings"),
+ LoginService = require("./login-service");
const KernalModeTrust = window.KernalModeTrust;
if(!KernalModeTrust){
@@ -183,6 +184,44 @@ define(function (require, exports, module) {
_setupDocumentClickHandler();
}
+ /**
+ * Update main navigation branding based on entitlements
+ */
+ function _updateBranding(entitlements) {
+ const $brandingLink = $("#phcode-io-main-nav");
+ if (!entitlements) {
+ Phoenix.pro.plan = {
+ paidSubscriber: false,
+ name: "Community Edition",
+ isInTrial: false
+ };
+ return;
+ }
+
+ if (entitlements && entitlements.plan){
+ Phoenix.pro.plan = {
+ paidSubscriber: entitlements.plan.paidSubscriber,
+ name: entitlements.plan.name,
+ isInTrial: entitlements.plan.isInTrial,
+ validTill: entitlements.plan.validTill
+ };
+ }
+ if (entitlements && entitlements.plan && entitlements.plan.paidSubscriber) {
+ // Paid subscriber: show plan name with feather icon
+ const planName = entitlements.plan.name || "Phoenix Pro";
+ $brandingLink
+ .attr("href", "https://account.phcode.dev")
+ .addClass("phoenix-pro")
+ .html(`${planName}
`);
+ } else {
+ // Free user: show phcode.io branding
+ $brandingLink
+ .attr("href", "https://phcode.io")
+ .removeClass("phoenix-pro")
+ .text("phcode.io");
+ }
+ }
+
let userEmail="";
class SecureEmail extends HTMLElement {
constructor() {
@@ -284,6 +323,50 @@ define(function (require, exports, module) {
}
}
+ /**
+ * Update popup content with entitlements data
+ */
+ function _updatePopupWithEntitlements(entitlements) {
+ if (!$popup || !entitlements) {
+ return;
+ }
+
+ // 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
+ $planName.removeClass('user-plan-free user-plan-paid');
+ const planClass = entitlements.plan.paidSubscriber ? 'user-plan-paid' : 'user-plan-free';
+ $planName.addClass(planClass);
+ }
+
+ // Update quota section if available
+ if (entitlements.profileview && entitlements.profileview.quota) {
+ const $quotaSection = $popup.find('.quota-section');
+ const quota = entitlements.profileview.quota;
+
+ // Remove forced-hidden and show quota section
+ $quotaSection.removeClass('forced-hidden');
+
+ // Update quota content
+ $quotaSection.find('.titleText').text(quota.titleText);
+ $quotaSection.find('.usageText').text(quota.usageText);
+ $quotaSection.find('.progress-fill').css('width', quota.usedPercent + '%');
+ }
+
+ // Update HTML message if available
+ if (entitlements.profileview && entitlements.profileview.htmlMessage) {
+ const $htmlMessageSection = $popup.find('.html-message');
+ $htmlMessageSection.removeClass('forced-hidden');
+ $htmlMessageSection.html(entitlements.profileview.htmlMessage);
+ }
+
+ // Reposition popup after content changes
+ positionPopup();
+ }
+
/**
* Shows the user profile popup when the user is logged in
*/
@@ -296,26 +379,40 @@ define(function (require, exports, module) {
const profileData = KernalModeTrust.loginService.getProfile();
userEmail = profileData.email;
userName = profileData.firstName + " " + profileData.lastName;
+
+ // Default template data (fallback) - start with cached plan info if available
const templateData = {
initials: profileData.profileIcon.initials,
avatarColor: profileData.profileIcon.color,
- planClass: "user-plan-free", // "user-plan-paid" for paid plan
+ planClass: "user-plan-free",
planName: "Free Plan",
- quotaUsed: "7,000",
- quotaTotal: "10,000",
- quotaUnit: "tokens",
- quotaPercent: 70,
+ titleText: "Ai Quota Used",
+ usageText: "100 / 200 credits",
+ usedPercent: 0,
Strings: Strings
};
- // Render template with data
+ // Note: We don't await here to keep popup display instant
+ // Cached entitlements will be applied asynchronously after popup is shown
+
+ // Render template with data immediately
const renderedTemplate = Mustache.render(profileTemplate, templateData);
$popup = $(renderedTemplate);
$("body").append($popup);
isPopupVisible = true;
+
positionPopup();
+ // Apply cached entitlements immediately if available (including quota/messages)
+ KernalModeTrust.loginService.getEntitlements(false).then(cachedEntitlements => {
+ if (cachedEntitlements && isPopupVisible) {
+ _updatePopupWithEntitlements(cachedEntitlements);
+ }
+ }).catch(error => {
+ console.error('Failed to apply cached entitlements to popup:', error);
+ });
+
PopUpManager.addPopUp($popup, function() {
$popup.remove();
$popup = null;
@@ -347,6 +444,26 @@ define(function (require, exports, module) {
// Load user details iframe for browser apps (after popup is created)
_loadUserDetailsIframe();
+
+ // Refresh entitlements in background and update popup if still visible
+ _refreshEntitlementsInBackground();
+ }
+
+ /**
+ * Refresh entitlements in background and update popup if still visible
+ */
+ async function _refreshEntitlementsInBackground() {
+ try {
+ // Fetch fresh entitlements from API
+ const freshEntitlements = await KernalModeTrust.loginService.getEntitlements(true); // Force refresh to get latest data
+
+ // Only update popup if it's still visible
+ if (isPopupVisible && $popup && freshEntitlements) {
+ _updatePopupWithEntitlements(freshEntitlements);
+ }
+ } catch (error) {
+ console.error('Failed to refresh entitlements in background:', error);
+ }
}
/**
@@ -421,6 +538,12 @@ define(function (require, exports, module) {
closePopup();
}
_removeProfileIcon();
+
+ // Clear cached entitlements when user logs out
+ LoginService.clearEntitlements();
+
+ // Reset branding to free mode
+ _updateBranding(null);
}
function setLoggedIn(initial, color) {
@@ -429,6 +552,13 @@ define(function (require, exports, module) {
closePopup();
}
_updateProfileIcon(initial, color);
+
+ // Preload entitlements when user logs in
+ KernalModeTrust.loginService.getEntitlements()
+ .then(_updateBranding)
+ .catch(error => {
+ console.error('Failed to preload entitlements on login:', error);
+ });
}
exports.init = init;
diff --git a/src/styles/UserProfile.less b/src/styles/UserProfile.less
index 2e9daf3056..cc8b069d87 100644
--- a/src/styles/UserProfile.less
+++ b/src/styles/UserProfile.less
@@ -73,7 +73,7 @@
font-size: 12px;
}
- .quota-section {
+ .profile-section {
margin-bottom: 20px;
}
diff --git a/src/styles/brackets.less b/src/styles/brackets.less
index f7100dee51..933ae3a304 100644
--- a/src/styles/brackets.less
+++ b/src/styles/brackets.less
@@ -82,6 +82,15 @@ html, body {
color: @dark-bc-text-link;
}
+#phcode-io-main-nav.phoenix-pro{
+ background: @phoenix-pro-gradient;
+ background-clip: text;
+ -webkit-background-clip: text; /* Chrome, Safari */
+ color: transparent; /* works in Firefox */
+ -webkit-text-fill-color: transparent; /* Chrome, Safari */
+ display: inline-block;
+}
+
#ctrl-nav-overlay {
position: absolute;
display: flex; /* Enable flexbox */
diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less
index 71fc73aa40..8221598fbd 100644
--- a/src/styles/brackets_core_ui_variables.less
+++ b/src/styles/brackets_core_ui_variables.less
@@ -279,3 +279,12 @@
// CSS Codehint icon
@css-codehint-icon: #2ea56c;
@dark-css-codehint-icon: #146a41;
+
+// phoenix pro
+@phoenix-pro-gradient: linear-gradient(
+ 45deg,
+ #ff8c42, /* deep orange */
+ #ffa500, /* bright orange */
+ #ffcc70, /* golden yellow */
+ #ffd700 /* rich gold */
+);
diff --git a/test/SpecRunner.html b/test/SpecRunner.html
index 21fc3bb5e8..feaaf3ecd2 100644
--- a/test/SpecRunner.html
+++ b/test/SpecRunner.html
@@ -270,6 +270,7 @@
isSpecRunnerWindow: window.location.pathname.endsWith("/SpecRunner.html"),
firstBoot: false, // will be set below
startTime: Date.now(),
+ pro: {},
TRUSTED_ORIGINS: {
// if modifying this list, make sure to update in https://github.com/phcode-dev/phcode.live/blob/main/docs/trustedOrigins.js
// extensions may add their trusted origin to this list at any time.