From f7e7f76d0fd6d40c54d8945b99791b5d7fbcd8f9 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 1 Sep 2025 13:26:22 +0530 Subject: [PATCH 1/6] fix: pro logo doesnt go away on logout on top left navbar --- src/index.html | 2 +- src/services/profile-menu.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.html b/src/index.html index 9e287c2feb..13f8831710 100644 --- a/src/index.html +++ b/src/index.html @@ -350,7 +350,7 @@ baseURL: _getBaseURL(), isTestWindow: _isTestWindow(), firstBoot: false, // will be set below - pro: {}, + pro: {}, // this is only for display purposes and should not be used to gate features. use kernal mode for that startTime: Date.now(), TRUSTED_ORIGINS: { // if modifying this list, make sure to update in https://github.com/phcode-dev/phcode.live/blob/main/docs/trustedOrigins.js diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index 7390539a11..151451c9c7 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -190,12 +190,13 @@ define(function (require, exports, module) { function _updateBranding(entitlements) { const $brandingLink = $("#phcode-io-main-nav"); if (!entitlements) { + // Phoenix.pro is only for display purposes and should not be used to gate features. + // Use kernal mode apis for trusted check of pro features. Phoenix.pro.plan = { paidSubscriber: false, name: "Community Edition", isInTrial: false }; - return; } if (entitlements && entitlements.plan){ From 33be5f6153729bfccc012fb680504f0e80c78377 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 2 Sep 2025 10:45:26 +0530 Subject: [PATCH 2/6] feat: add promotions infra --- src/phoenix/trust_ring.js | 2 + src/services/login-service.js | 1 + src/services/profile-menu.js | 4 +- src/services/promotions.js | 280 ++++++++++++++++++++++++++++++++++ src/utils/Metrics.js | 3 +- test/spec/SpecRunnerUtils.js | 8 + 6 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 src/services/promotions.js diff --git a/src/phoenix/trust_ring.js b/src/phoenix/trust_ring.js index 0cd11fa29f..019d47f4e3 100644 --- a/src/phoenix/trust_ring.js +++ b/src/phoenix/trust_ring.js @@ -179,6 +179,7 @@ function _selectKeys() { } const CRED_KEY_API = "API_KEY"; +const CRED_KEY_ENTITLEMENTS = "ENTITLEMENTS_GRANT_KEY"; const { key, iv } = _selectKeys(); // this key is set at boot time as a truct base for all the core components before any extensions are loaded. // just before extensions are loaded, this key is blanked. This can be used by core modules to talk with other @@ -186,6 +187,7 @@ const { key, iv } = _selectKeys(); // KernalModeTrust should only be available within all code that loads before the first default/any extension. window.KernalModeTrust = { CRED_KEY_API, + CRED_KEY_ENTITLEMENTS, aesKeys: { key, iv }, setCredential, getCredential, diff --git a/src/services/login-service.js b/src/services/login-service.js index 96a1cf765a..bc1ff66211 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -24,6 +24,7 @@ */ define(function (require, exports, module) { + const Promotions = require("./promotions"); const KernalModeTrust = window.KernalModeTrust; if(!KernalModeTrust){ diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index 151451c9c7..bb8f3916e8 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -194,8 +194,7 @@ define(function (require, exports, module) { // Use kernal mode apis for trusted check of pro features. Phoenix.pro.plan = { paidSubscriber: false, - name: "Community Edition", - isInTrial: false + name: "Community Edition" }; } @@ -203,7 +202,6 @@ define(function (require, exports, module) { Phoenix.pro.plan = { paidSubscriber: entitlements.plan.paidSubscriber, name: entitlements.plan.name, - isInTrial: entitlements.plan.isInTrial, validTill: entitlements.plan.validTill }; } diff --git a/src/services/promotions.js b/src/services/promotions.js new file mode 100644 index 0000000000..edaef78682 --- /dev/null +++ b/src/services/promotions.js @@ -0,0 +1,280 @@ +/* + * 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. + * + */ + +/*global logger*/ + +/** + * Promotions Service + * + * Manages pro trial promotions for both native and browser applications. + * Provides loginless pro trials + * + * - First install: 30-day trial on first usage + * - Subsequent versions: 3-day trial (or remaining from 30-day if still valid) + * - Older versions: No new trial, but existing 30-day trial remains valid + */ + +define(function (require, exports, module) { + + const EventDispatcher = require("utils/EventDispatcher"), + Metrics = require("utils/Metrics"), + semver = require("thirdparty/semver.browser"); + + const KernalModeTrust = window.KernalModeTrust; + if (!KernalModeTrust) { + throw new Error("Promotions service requires access to KernalModeTrust. Cannot boot without trust ring"); + } + + // Make this module an event dispatcher + EventDispatcher.makeEventDispatcher(exports); + + // Constants + const EVENT_PRO_UPGRADE_ON_INSTALL = "pro_upgrade_on_install"; + const TRIAL_POLL_MS = 10 * 1000; // 10 seconds after start, we assign a free trial if possible + const FIRST_INSTALL_TRIAL_DAYS = 30; + const SUBSEQUENT_TRIAL_DAYS = 3; + const MS_PER_DAY = 24 * 60 * 60 * 1000; + + /** + * Generate SHA-256 signature for trial data integrity + */ + async function _generateSignature(proVersion, endDate) { + const salt = window.AppConfig ? window.AppConfig.version : "default-salt"; + const data = proVersion + "|" + endDate + "|" + salt; + + // Use browser crypto API for SHA-256 hashing + const encoder = new TextEncoder(); + const dataBuffer = encoder.encode(data); + const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // hash hex string + } + + /** + * Validate trial data signature + */ + async function _isValidSignature(trialData) { + if (!trialData.signature || !trialData.proVersion || !trialData.endDate) { + return false; + } + + const expectedSignature = await _generateSignature(trialData.proVersion, trialData.endDate); + return trialData.signature === expectedSignature; + } + + /** + * Get stored trial data with validation + */ + async function _getTrialData() { + try { + if (Phoenix.isNativeApp) { + // Native app: use KernalModeTrust credential store + const data = await KernalModeTrust.getCredential(KernalModeTrust.CRED_KEY_ENTITLEMENTS); + if (!data) { + return null; + } + const parsed = JSON.parse(data); + return (await _isValidSignature(parsed)) ? parsed : null; + } else { + // Browser app: use virtual filesystem + return new Promise((resolve) => { + // app support dir in browser is /fs/app/ + const filePath = Phoenix.app.getApplicationSupportDirectory() + "entitlements_granted.json"; + window.fs.readFile(filePath, 'utf8', function (err, data) { + if (err || !data) { + resolve(null); + return; + } + try { + const parsed = JSON.parse(data); + _isValidSignature(parsed).then(isValid => { + resolve(isValid ? parsed : null); + }).catch(() => resolve(null)); + } catch (e) { + resolve(null); + } + }); + }); + } + } catch (error) { + console.error("Error getting trial data:", error); + return null; + } + } + + /** + * Store trial data with signature + */ + async function _setTrialData(trialData) { + trialData.signature = await _generateSignature(trialData.proVersion, trialData.endDate); + + try { + if (Phoenix.isNativeApp) { + // Native app: use KernalModeTrust credential store + await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_ENTITLEMENTS, JSON.stringify(trialData)); + } else { + // Browser app: use virtual filesystem + return new Promise((resolve, reject) => { + const filePath = Phoenix.app.getApplicationSupportDirectory() + "entitlements_granted.json"; + window.fs.writeFile(filePath, JSON.stringify(trialData), 'utf8', (writeErr) => { + if (writeErr) { + console.error("Error storing trial data:", writeErr); + reject(writeErr); + } else { + resolve(); + } + }); + }); + } + } catch (error) { + console.error("Error setting trial data:", error); + throw error; + } + } + + /** + * Calculate remaining trial days from end date + */ + function _calculateRemainingTrialDays(existingTrialData) { + const now = Date.now(); + const trialEndDate = existingTrialData.endDate; + + // Calculate days remaining until trial ends + const msRemaining = trialEndDate - now; + return Math.max(0, Math.ceil(msRemaining / MS_PER_DAY)); // days remaining + } + + /** + * Check if version1 is newer than version2 using semver + */ + function _isNewerVersion(version1, version2) { + try { + return semver.gt(version1, version2); + } catch (error) { + console.error("Error comparing versions:", error, version1, version2); + // Assume not newer if comparison fails + return false; + } + } + + /** + * Check if pro trial is currently activated + */ + async function isProTrialActivated() { + const trialData = await _getTrialData(); + if (!trialData) { + return false; + } + + const remainingDays = _calculateRemainingTrialDays(trialData); + + return remainingDays > 0; + } + + async function activateProTrial() { + const currentVersion = window.AppConfig ? window.AppConfig.apiVersion : "1.0.0"; + const existingTrialData = await _getTrialData(); + + let trialDays = FIRST_INSTALL_TRIAL_DAYS; + let endDate; + const now = Date.now(); + let metricString = `${currentVersion.replaceAll(".", "_")}`; // 3.1.0 -> 3_1_0 + + if (existingTrialData) { + // Existing trial found + const remainingDays = _calculateRemainingTrialDays(existingTrialData); + const trialVersion = existingTrialData.proVersion; + const isNewerVersion = _isNewerVersion(currentVersion, trialVersion); + + // Check if we should grant any trial + if (remainingDays <= 0 && !isNewerVersion) { + console.log("Existing trial expired, same/older version - no new trial"); + return; + } + + // Determine trial days and end date + if (isNewerVersion) { + if (remainingDays >= SUBSEQUENT_TRIAL_DAYS) { + // Newer version but existing trial is longer - keep existing + console.log(`Newer version, keeping existing trial (${remainingDays} days)`); + trialDays = remainingDays; + endDate = existingTrialData.endDate; + metricString = `nD_${metricString}_upgrade`; + } else { + // Newer version with shorter existing trial - give 3 days + console.log(`Newer version - granting ${SUBSEQUENT_TRIAL_DAYS} days trial`); + trialDays = SUBSEQUENT_TRIAL_DAYS; + endDate = now + (trialDays * MS_PER_DAY); + metricString = `3D_${metricString}`; + } + } else { + // Same/older version: keep existing trial - no changes needed + console.log(`Same/older version - keeping existing ${remainingDays} day trial.`); + return; + } + } else { + // First install - 30 days from now + endDate = now + (FIRST_INSTALL_TRIAL_DAYS * MS_PER_DAY); + metricString = `1Mo_${metricString}`; + } + + const trialData = { + proVersion: currentVersion, + endDate: endDate + }; + + await _setTrialData(trialData); + Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "trialAct", metricString); + Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "trial", "activated"); + console.log(`Pro trial activated for ${trialDays} days`); + + // Trigger the event for UI to handle + exports.trigger(EVENT_PRO_UPGRADE_ON_INSTALL, { + trialDays: trialDays, + isFirstInstall: !existingTrialData + }); + } + + function _isAnyDialogsVisible() { + const $modal = $(`.modal.instance`); + return $modal.length > 0 && $modal.is(':visible'); + } + + /** + * Start the pro trial activation process + * Waits 2 minutes, then triggers the upgrade event + */ + console.log(`Checking pro trial activation in ${TRIAL_POLL_MS / 1000} seconds...`); + + const trialActivatePoller = setInterval(()=> { + if(_isAnyDialogsVisible()){ + // maybe the user hasn't dismissed the new project dialog + return; + } + clearInterval(trialActivatePoller); + activateProTrial().catch(error => { + Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "trial", `errActivate`); + logger.reportError(error, "Error activating pro trial:"); + }); + }, TRIAL_POLL_MS); + + // Public exports + exports.isProTrialActivated = isProTrialActivated; + exports.EVENT_PRO_UPGRADE_ON_INSTALL = EVENT_PRO_UPGRADE_ON_INSTALL; + +}); diff --git a/src/utils/Metrics.js b/src/utils/Metrics.js index 8108cc79fd..7f5e21e720 100644 --- a/src/utils/Metrics.js +++ b/src/utils/Metrics.js @@ -120,7 +120,8 @@ define(function (require, exports, module) { NODEJS: "node", LINT: "lint", GIT: "git", - AUTH: "auth" + AUTH: "auth", + PRO: "pro" }; /** diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index 51869dc1e3..871d3938af 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -554,6 +554,10 @@ define(function (require, exports, module) { await awaitsForDone(promise); } + /** + * Waits for any modal dialogs to appear, waits for default for 2 seconds + * note: this is a test-only/short task function and shouldn't be used in production as this polls + */ async function waitForModalDialog(timeout=2000) { // Make sure there's one and only one dialog open await awaitsFor(()=>{ @@ -562,6 +566,10 @@ define(function (require, exports, module) { }, timeout); } + /** + * Waits for any modal dialogs to close, waits for default for 2 seconds + * note: this is a test-only/short task function and shouldn't be used in production as this polls + */ async function waitForNoModalDialog(timeout=2000) { // Make sure there's one and only one dialog open await awaitsFor(()=>{ From f893eeebfee85e817805ce74311a92eeb13310a7 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 2 Sep 2025 12:33:09 +0530 Subject: [PATCH 3/6] refactor: secure KernalModeTrust.loginService --- src/services/login-browser.js | 39 +++++++------------------- src/services/login-desktop.js | 37 +++++++++++-------------- src/services/login-service.js | 25 +++++++++-------- src/services/profile-menu.js | 4 ++- src/services/promotions.js | 16 +++++------ src/services/setup-login-service.js | 43 +++++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 70 deletions(-) create mode 100644 src/services/setup-login-service.js diff --git a/src/services/login-browser.js b/src/services/login-browser.js index be0c15fab0..8e0dd953cf 100644 --- a/src/services/login-browser.js +++ b/src/services/login-browser.js @@ -43,15 +43,14 @@ */ define(function (require, exports, module) { - const EventDispatcher = require("utils/EventDispatcher"), - PreferencesManager = require("preferences/PreferencesManager"), + require("./login-service"); // after this, loginService will be in KernalModeTrust + const PreferencesManager = require("preferences/PreferencesManager"), Metrics = require("utils/Metrics"), Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), 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"); @@ -60,11 +59,7 @@ define(function (require, exports, module) { // integrated extensions will have access to kernal mode, but not external extensions throw new Error("Browser Login service should have access to KernalModeTrust. Cannot boot without trust ring"); } - const secureExports = {}; - // Only set loginService for browser apps to avoid conflict with desktop login - if (!Phoenix.isNativeApp) { - KernalModeTrust.loginService = secureExports; - } + const LoginService = KernalModeTrust.loginService; // user profile structure: "customerID": "uuid...", "firstName":"Aa","lastName":"bb", // "email":"aaaa@sss.com", "loginTime":1750074393853, "isSuccess": true, @@ -75,14 +70,6 @@ define(function (require, exports, module) { // just used as trigger to notify different windows about user profile changes const PREF_USER_PROFILE_VERSION = "userProfileVersion"; - EventDispatcher.makeEventDispatcher(exports); - EventDispatcher.makeEventDispatcher(secureExports); - - const _EVT_PAGE_FOCUSED = "page_focused"; - $(window).focus(function () { - exports.trigger(_EVT_PAGE_FOCUSED); - }); - function isLoggedIn() { return isLoggedInUser; } @@ -199,7 +186,6 @@ define(function (require, exports, module) { } let loginWaitingDialog = null; - let focusCheckInterval = null; /** * Show waiting dialog with auto-detection and manual check options @@ -288,10 +274,6 @@ define(function (require, exports, module) { loginWaitingDialog.close(); loginWaitingDialog = null; } - if (focusCheckInterval) { - clearInterval(focusCheckInterval); - focusCheckInterval = null; - } $(window).off('focus.loginWaiting'); } @@ -413,14 +395,13 @@ define(function (require, exports, module) { if (!Phoenix.isNativeApp) { init(); // kernal exports - secureExports.isLoggedIn = isLoggedIn; - secureExports.signInToAccount = signInToBrowser; - secureExports.signOutAccount = signOutBrowser; - secureExports.getProfile = getProfile; - secureExports.verifyLoginStatus = () => _verifyBrowserLogin(false); - secureExports.getAccountBaseURL = _getAccountBaseURL; - secureExports.getEntitlements = LoginService.getEntitlements; - secureExports.EVENT_ENTITLEMENTS_CHANGED = LoginService.EVENT_ENTITLEMENTS_CHANGED; + // Add to existing KernalModeTrust.loginService from login-service.js + LoginService.isLoggedIn = isLoggedIn; + LoginService.signInToAccount = signInToBrowser; + LoginService.signOutAccount = signOutBrowser; + LoginService.getProfile = getProfile; + LoginService.verifyLoginStatus = () => _verifyBrowserLogin(false); + LoginService.getAccountBaseURL = _getAccountBaseURL; } // public exports diff --git a/src/services/login-desktop.js b/src/services/login-desktop.js index 163f0e25dc..8ead917b97 100644 --- a/src/services/login-desktop.js +++ b/src/services/login-desktop.js @@ -19,6 +19,8 @@ /*global logger*/ define(function (require, exports, module) { + require("./login-service"); // after this, loginService will be in KernalModeTrust + const EventDispatcher = require("utils/EventDispatcher"), PreferencesManager = require("preferences/PreferencesManager"), Metrics = require("utils/Metrics"), @@ -27,7 +29,6 @@ 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"); @@ -37,11 +38,8 @@ define(function (require, exports, module) { // 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"); } - const secureExports = {}; - // Only set loginService for native apps to avoid conflict with browser login - if (Phoenix.isNativeApp) { - KernalModeTrust.loginService = secureExports; - } + const LoginService = KernalModeTrust.loginService; + // user profile is something like "apiKey": "uuid...", validationCode: "dfdf", "firstName":"Aa","lastName":"bb", // "email":"aaaa@sss.com", "customerID":"uuid...","loginTime":1750074393853, // "profileIcon":{"color":"#14b8a6","initials":"AB"} @@ -51,12 +49,11 @@ define(function (require, exports, module) { // just used as trigger to notify different windows about user profile changes const PREF_USER_PROFILE_VERSION = "userProfileVersion"; - EventDispatcher.makeEventDispatcher(exports); - EventDispatcher.makeEventDispatcher(secureExports); - const _EVT_PAGE_FOCUSED = "page_focused"; + const focusWatcher = {}; + EventDispatcher.makeEventDispatcher(focusWatcher); $(window).focus(function () { - exports.trigger(_EVT_PAGE_FOCUSED); + focusWatcher.trigger(_EVT_PAGE_FOCUSED); }); const AUTH_CONNECTOR_ID = "ph_auth"; @@ -320,7 +317,7 @@ define(function (require, exports, module) { } } let isAutoSignedIn = false; - exports.on(_EVT_PAGE_FOCUSED, checkLoginStatus); + focusWatcher.on(_EVT_PAGE_FOCUSED, checkLoginStatus); async function _AutoSignedIn() { isAutoSignedIn = true; await checkLoginStatus(); @@ -329,7 +326,7 @@ define(function (require, exports, module) { // Clean up when dialog is closed dialog.done(function() { - exports.off(_EVT_PAGE_FOCUSED, checkLoginStatus); + focusWatcher.off(_EVT_PAGE_FOCUSED, checkLoginStatus); authNodeConnector.off(EVENT_CONNECTED, _AutoSignedIn); clearTimeout(closeTimeout); Metrics.countEvent(Metrics.EVENT_TYPE.AUTH, @@ -411,15 +408,13 @@ define(function (require, exports, module) { // Only set exports for native apps to avoid conflict with browser login if (Phoenix.isNativeApp) { init(); - // kernal exports - secureExports.isLoggedIn = isLoggedIn; - secureExports.signInToAccount = signInToAccount; - secureExports.signOutAccount = signOutAccount; - secureExports.getProfile = getProfile; - secureExports.verifyLoginStatus = () => _verifyLogin(false); - secureExports.getAccountBaseURL = getAccountBaseURL; - secureExports.getEntitlements = LoginService.getEntitlements; - secureExports.EVENT_ENTITLEMENTS_CHANGED = LoginService.EVENT_ENTITLEMENTS_CHANGED; + // kernal exports - add to existing KernalModeTrust.loginService from login-service.js + LoginService.isLoggedIn = isLoggedIn; + LoginService.signInToAccount = signInToAccount; + LoginService.signOutAccount = signOutAccount; + LoginService.getProfile = getProfile; + LoginService.verifyLoginStatus = () => _verifyLogin(false); + LoginService.getAccountBaseURL = getAccountBaseURL; } // public exports diff --git a/src/services/login-service.js b/src/services/login-service.js index bc1ff66211..6fe2d6b271 100644 --- a/src/services/login-service.js +++ b/src/services/login-service.js @@ -24,7 +24,8 @@ */ define(function (require, exports, module) { - const Promotions = require("./promotions"); + require("./setup-login-service"); // this adds loginService to KernalModeTrust + require("./promotions"); const KernalModeTrust = window.KernalModeTrust; if(!KernalModeTrust){ @@ -32,6 +33,8 @@ define(function (require, exports, module) { throw new Error("Login service should have access to KernalModeTrust. Cannot boot without trust ring"); } + const LoginService = KernalModeTrust.loginService; + // Event constants const EVENT_ENTITLEMENTS_CHANGED = "entitlements_changed"; @@ -44,7 +47,7 @@ define(function (require, exports, module) { */ async function getEntitlements(forceRefresh = false) { // Return null if not logged in - if (!KernalModeTrust.loginService.isLoggedIn()) { + if (!LoginService.isLoggedIn()) { return null; } @@ -54,7 +57,7 @@ define(function (require, exports, module) { } try { - const accountBaseURL = KernalModeTrust.loginService.getAccountBaseURL(); + const accountBaseURL = LoginService.getAccountBaseURL(); const language = Phoenix.app && Phoenix.app.language ? Phoenix.app.language : 'en'; let url = `${accountBaseURL}/getAppEntitlements?lang=${language}`; let fetchOptions = { @@ -67,7 +70,7 @@ define(function (require, exports, module) { // Handle different authentication methods for browser vs desktop if (Phoenix.isNativeApp) { // Desktop app: use appSessionID and validationCode - const profile = KernalModeTrust.loginService.getProfile(); + const profile = LoginService.getProfile(); if (profile && profile.apiKey && profile.validationCode) { url += `&appSessionID=${encodeURIComponent(profile.apiKey)}&validationCode=${encodeURIComponent(profile.validationCode)}`; } else { @@ -91,7 +94,7 @@ define(function (require, exports, module) { // Trigger event if entitlements changed if (entitlementsChanged) { - KernalModeTrust.loginService.trigger(EVENT_ENTITLEMENTS_CHANGED, result); + LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED, result); } return cachedEntitlements; @@ -113,14 +116,14 @@ define(function (require, exports, module) { cachedEntitlements = null; // Trigger event when entitlements are cleared - if (KernalModeTrust.loginService.trigger) { - KernalModeTrust.loginService.trigger(EVENT_ENTITLEMENTS_CHANGED, null); + if (LoginService.trigger) { + LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED, null); } } } - // Exports - exports.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED; - exports.getEntitlements = getEntitlements; - exports.clearEntitlements = clearEntitlements; + // Add functions to secure exports + LoginService.getEntitlements = getEntitlements; + LoginService.clearEntitlements = clearEntitlements; + LoginService.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED; }); diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index bb8f3916e8..045a42c8d1 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -539,7 +539,7 @@ define(function (require, exports, module) { _removeProfileIcon(); // Clear cached entitlements when user logs out - LoginService.clearEntitlements(); + KernalModeTrust.loginService.clearEntitlements(); // Reset branding to free mode _updateBranding(null); @@ -563,4 +563,6 @@ define(function (require, exports, module) { exports.init = init; exports.setNotLoggedIn = setNotLoggedIn; exports.setLoggedIn = setLoggedIn; + + // dont public exports things that extensions can use to get/put credentials and entitlements, display mods is fine }); diff --git a/src/services/promotions.js b/src/services/promotions.js index edaef78682..4fbfe938fe 100644 --- a/src/services/promotions.js +++ b/src/services/promotions.js @@ -31,8 +31,8 @@ define(function (require, exports, module) { - const EventDispatcher = require("utils/EventDispatcher"), - Metrics = require("utils/Metrics"), + require("./setup-login-service"); // this adds loginService to KernalModeTrust + const Metrics = require("utils/Metrics"), semver = require("thirdparty/semver.browser"); const KernalModeTrust = window.KernalModeTrust; @@ -40,8 +40,7 @@ define(function (require, exports, module) { throw new Error("Promotions service requires access to KernalModeTrust. Cannot boot without trust ring"); } - // Make this module an event dispatcher - EventDispatcher.makeEventDispatcher(exports); + const LoginService = KernalModeTrust.loginService; // Constants const EVENT_PRO_UPGRADE_ON_INSTALL = "pro_upgrade_on_install"; @@ -244,7 +243,7 @@ define(function (require, exports, module) { console.log(`Pro trial activated for ${trialDays} days`); // Trigger the event for UI to handle - exports.trigger(EVENT_PRO_UPGRADE_ON_INSTALL, { + LoginService.trigger(EVENT_PRO_UPGRADE_ON_INSTALL, { trialDays: trialDays, isFirstInstall: !existingTrialData }); @@ -273,8 +272,9 @@ define(function (require, exports, module) { }); }, TRIAL_POLL_MS); - // Public exports - exports.isProTrialActivated = isProTrialActivated; - exports.EVENT_PRO_UPGRADE_ON_INSTALL = EVENT_PRO_UPGRADE_ON_INSTALL; + // Add to secure exports + LoginService.isProTrialActivated = isProTrialActivated; + LoginService.EVENT_PRO_UPGRADE_ON_INSTALL = EVENT_PRO_UPGRADE_ON_INSTALL; + // no public exports to prevent extension tampering }); diff --git a/src/services/setup-login-service.js b/src/services/setup-login-service.js new file mode 100644 index 0000000000..1c8804d22d --- /dev/null +++ b/src/services/setup-login-service.js @@ -0,0 +1,43 @@ +/* + * 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 kernal mode trust setup + * + * This module contains shared login service functionality used by both + * browser and desktop login implementations, including entitlements management. + */ + +define(function (require, exports, module) { + const EventDispatcher = require("utils/EventDispatcher"); + + 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"); + } + + // Create secure exports and set up KernalModeTrust.loginService + const secureExports = {}; + EventDispatcher.makeEventDispatcher(secureExports); + + // Set up loginService for both native and browser apps + KernalModeTrust.loginService = secureExports; + + // no public exports to prevent extension tampering +}); From d2f0d327f5fe4ec52ff944ba314ee19e18baa2c6 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 2 Sep 2025 12:54:17 +0530 Subject: [PATCH 4/6] fix: clicking on phoenix pro title on new project window does nothing --- src/assets/new-project/assets/js/code-editor.js | 6 +++++- src/assets/new-project/code-editor.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/assets/new-project/assets/js/code-editor.js b/src/assets/new-project/assets/js/code-editor.js index d8e82c4033..22de25f873 100644 --- a/src/assets/new-project/assets/js/code-editor.js +++ b/src/assets/new-project/assets/js/code-editor.js @@ -186,7 +186,7 @@ function _openURLInTauri(url) { function _updateProBranding() { try { const $freeTitle = $('.phoenix-free-title'); - const $proTitle = $('.phoenix-pro-title'); + const $proTitle = $('#phoenix-pro-title'); const $proTitleSpan = $('.pro-plan-name'); if (!$freeTitle.length || !$proTitle.length || !$proTitleSpan.length) { @@ -222,6 +222,10 @@ function initCodeEditor() { Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "main.Click", "viewMore"); window.location.href = 'new-project-more.html'; }; + document.getElementById("phoenix-pro-title").onclick = function() { + Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "main.Click", "proTitle"); + _openURLInTauri("https://account.phcode.dev"); + }; document.getElementById("githubStarsButton").onclick = function() { Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "main.Click", "githubStars"); _openURLInTauri("https://github.com/phcode-dev/phoenix"); diff --git a/src/assets/new-project/code-editor.html b/src/assets/new-project/code-editor.html index 6362e1adf2..eb0f86a489 100644 --- a/src/assets/new-project/code-editor.html +++ b/src/assets/new-project/code-editor.html @@ -16,7 +16,7 @@ + + + + + + + diff --git a/src/services/pro-dialogs.js b/src/services/pro-dialogs.js new file mode 100644 index 0000000000..6ae51b12ee --- /dev/null +++ b/src/services/pro-dialogs.js @@ -0,0 +1,53 @@ +/* + * 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. + * + */ + +/*global logger*/ + +/** + * Phoenix pro pre and post promo dialogs + * shows dialog where we give Phoenix pro to all users on app install + * and dialogs on pro trial ends. + * + */ + +define(function (require, exports, module) { + const proTitle = ` + Phoenix Pro + + `; + require("./setup-login-service"); // this adds loginService to KernalModeTrust + const Dialogs = require("widgets/Dialogs"), + Mustache = require("thirdparty/mustache/mustache"), + Strings = require("strings"), + StringUtils = require("utils/StringUtils"), + proUpgradeHTML = require("text!./html/pro-upgrade.html"); + + function showProUpgradeDialog(trialDays) { + const title = StringUtils.format(Strings.PROMO_UPGRADE_TITLE, proTitle); + const message = StringUtils.format(Strings.PROMO_UPGRADE_MESSAGE, trialDays); + const $template = $(Mustache.render(proUpgradeHTML, {title, message, Strings})); + Dialogs.showModalDialogUsingTemplate($template).done(function (id) { + console.log("Dialog closed with id: " + id); + if(id === 'learn_more') { + Phoenix.app.openURLInDefaultBrowser(brackets.config.homepage_url); + } + }); + } + + exports.showProUpgradeDialog = showProUpgradeDialog; +}); diff --git a/src/services/promotions.js b/src/services/promotions.js index 4fbfe938fe..dff787f786 100644 --- a/src/services/promotions.js +++ b/src/services/promotions.js @@ -33,7 +33,8 @@ define(function (require, exports, module) { require("./setup-login-service"); // this adds loginService to KernalModeTrust const Metrics = require("utils/Metrics"), - semver = require("thirdparty/semver.browser"); + semver = require("thirdparty/semver.browser"), + ProDialogs = require("./pro-dialogs"); const KernalModeTrust = window.KernalModeTrust; if (!KernalModeTrust) { @@ -242,6 +243,7 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "trial", "activated"); console.log(`Pro trial activated for ${trialDays} days`); + ProDialogs.showProUpgradeDialog(trialDays); // Trigger the event for UI to handle LoginService.trigger(EVENT_PRO_UPGRADE_ON_INSTALL, { trialDays: trialDays, diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 933ae3a304..b16a4208d3 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -83,7 +83,7 @@ html, body { } #phcode-io-main-nav.phoenix-pro{ - background: @phoenix-pro-gradient; + background: @phoenix-pro-gradient-dark; background-clip: text; -webkit-background-clip: text; /* Chrome, Safari */ color: transparent; /* works in Firefox */ diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less index 8221598fbd..0417784d97 100644 --- a/src/styles/brackets_core_ui_variables.less +++ b/src/styles/brackets_core_ui_variables.less @@ -281,10 +281,19 @@ @dark-css-codehint-icon: #146a41; // phoenix pro -@phoenix-pro-gradient: linear-gradient( +@phoenix-pro-gradient-dark: linear-gradient( 45deg, #ff8c42, /* deep orange */ #ffa500, /* bright orange */ #ffcc70, /* golden yellow */ #ffd700 /* rich gold */ ); + +@phoenix-pro-gradient-light: linear-gradient( + 45deg, + #cc5500 0%, /* burnt orange */ + #ff7a1a 25%, /* strong orange */ + #ff9900 50%, /* vibrant orange-gold */ + #e6a600 75%, /* darker amber */ + #c99700 100% /* rich golden brown */ +); From ef53c1f8b3d47f973437369362a01e2dcc836324 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 2 Sep 2025 19:11:28 +0530 Subject: [PATCH 6/6] chore: phoenix pro dialog light theme support --- src/services/html/pro-upgrade.html | 96 +------------- src/styles/brackets.less | 1 + src/styles/brackets_core_ui_variables.less | 2 +- src/styles/phoenix-pro.less | 140 +++++++++++++++++++++ 4 files changed, 143 insertions(+), 96 deletions(-) create mode 100644 src/styles/phoenix-pro.less diff --git a/src/services/html/pro-upgrade.html b/src/services/html/pro-upgrade.html index 79696b8dbe..323aa91509 100644 --- a/src/services/html/pro-upgrade.html +++ b/src/services/html/pro-upgrade.html @@ -1,105 +1,11 @@