@@ -168427,6 +168427,7 @@ define("services/EntitlementsManager", function (require, exports, module) {
168427168427
168428168428 const EventDispatcher = require("utils/EventDispatcher"),
168429168429 AIControl = require("./ai-control"),
168430+ UserNotifications = require("./UserNotifications"),
168430168431 Strings = require("strings"),
168431168432 StringUtils = require("utils/StringUtils");
168432168433
@@ -168528,6 +168529,36 @@ define("services/EntitlementsManager", function (require, exports, module) {
168528168529 return await LoginService.getEntitlements();
168529168530 }
168530168531
168532+ /**
168533+ * Get notifications array from entitlements. Uses cached entitlements when available.
168534+ * Notifications are used to display in-app promotional messages, alerts, or announcements to users.
168535+ *
168536+ * @returns {Promise<Array>} Array of notification objects, empty array if no notifications available
168537+ *
168538+ * @description Each notification object has the following structure:
168539+ * ```javascript
168540+ * {
168541+ * notificationID: string, // Unique UUID to track if notification was shown
168542+ * title: string, // Notification title
168543+ * htmlContent: string, // HTML content of the notification message
168544+ * validTill: number, // Timestamp when notification expires
168545+ * options: {
168546+ * autoCloseTimeS: number, // Optional: Time in seconds for auto-close. Default: never
168547+ * dismissOnClick: boolean, // Optional: Close on click. Default: true
168548+ * toastStyle: string // Optional: Style class (NOTIFICATION_STYLES_CSS_CLASS.INFO, etc.)
168549+ * }
168550+ * }
168551+ * ```
168552+ */
168553+ async function getNotifications() {
168554+ const entitlements = await _getEffectiveEntitlements();
168555+
168556+ if (entitlements && entitlements.notifications) {
168557+ return entitlements.notifications;
168558+ }
168559+ return [];
168560+ }
168561+
168531168562 /**
168532168563 * Get live edit is enabled for user, based on his logged in pro-user/trial status.
168533168564 *
@@ -168692,6 +168723,7 @@ define("services/EntitlementsManager", function (require, exports, module) {
168692168723 EntitlementsManager.trigger(EVENT_ENTITLEMENTS_CHANGED);
168693168724 });
168694168725 AIControl.init();
168726+ UserNotifications.init();
168695168727 }
168696168728
168697168729 // Test-only exports for integration testing
@@ -168703,6 +168735,7 @@ define("services/EntitlementsManager", function (require, exports, module) {
168703168735 isInProTrial,
168704168736 getTrialRemainingDays,
168705168737 getRawEntitlements,
168738+ getNotifications,
168706168739 getLiveEditEntitlement,
168707168740 loginToAccount
168708168741 };
@@ -168718,11 +168751,313 @@ define("services/EntitlementsManager", function (require, exports, module) {
168718168751 EntitlementsManager.isInProTrial = isInProTrial;
168719168752 EntitlementsManager.getTrialRemainingDays = getTrialRemainingDays;
168720168753 EntitlementsManager.getRawEntitlements = getRawEntitlements;
168754+ EntitlementsManager.getNotifications = getNotifications;
168721168755 EntitlementsManager.getLiveEditEntitlement = getLiveEditEntitlement;
168722168756 EntitlementsManager.getAIEntitlement = getAIEntitlement;
168723168757 EntitlementsManager.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED;
168724168758});
168725168759
168760+ /*
168761+ * GNU AGPL-3.0 License
168762+ *
168763+ * Copyright (c) 2021 - present core.ai . All rights reserved.
168764+ *
168765+ * This program is free software: you can redistribute it and/or modify it under
168766+ * the terms of the GNU Affero General Public License as published by the Free
168767+ * Software Foundation, either version 3 of the License, or (at your option) any later version.
168768+ *
168769+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
168770+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
168771+ * See the GNU Affero General Public License for more details.
168772+ *
168773+ * You should have received a copy of the GNU Affero General Public License along
168774+ * with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
168775+ *
168776+ */
168777+
168778+ /**
168779+ * User Notifications Service
168780+ *
168781+ * This module handles server-sent notifications from the entitlements API.
168782+ * Notifications are displayed as toast messages and acknowledged back to the server.
168783+ */
168784+
168785+ define("services/UserNotifications", function (require, exports, module) {
168786+ const KernalModeTrust = window.KernalModeTrust;
168787+ if(!KernalModeTrust){
168788+ throw new Error("UserNotifications should have access to KernalModeTrust. Cannot boot without trust ring");
168789+ }
168790+
168791+ const PreferencesManager = require("preferences/PreferencesManager"),
168792+ NotificationUI = require("widgets/NotificationUI");
168793+
168794+ const PREF_NOTIFICATIONS_SHOWN_LIST = "notificationsShownList";
168795+ PreferencesManager.stateManager.definePreference(PREF_NOTIFICATIONS_SHOWN_LIST, "object", {});
168796+
168797+ let EntitlementsManager;
168798+ let LoginService;
168799+
168800+ // In-memory tracking to prevent duplicate notifications during rapid EVENT_ENTITLEMENTS_CHANGED events
168801+ const currentlyShownNotifications = new Set();
168802+
168803+ // Save a copy of window.fetch so that extensions won't tamper with it
168804+ let fetchFn = window.fetch;
168805+
168806+ /**
168807+ * Get the list of notification IDs that have been shown and acknowledged
168808+ * @returns {Object} Map of notificationID -> timestamp
168809+ */
168810+ function getShownNotifications() {
168811+ return PreferencesManager.stateManager.get(PREF_NOTIFICATIONS_SHOWN_LIST) || {};
168812+ }
168813+
168814+ /**
168815+ * Mark a notification as shown and acknowledged
168816+ * @param {string} notificationID - The notification ID to mark as shown
168817+ */
168818+ function markNotificationAsShown(notificationID) {
168819+ const shownNotifications = getShownNotifications();
168820+ shownNotifications[notificationID] = Date.now();
168821+ PreferencesManager.stateManager.set(PREF_NOTIFICATIONS_SHOWN_LIST, shownNotifications);
168822+ currentlyShownNotifications.delete(notificationID);
168823+ }
168824+
168825+ /**
168826+ * Call the server API to acknowledge a notification
168827+ * @param {string} notificationID - The notification ID to acknowledge
168828+ * @returns {Promise<boolean>} - True if successful, false otherwise
168829+ */
168830+ async function acknowledgeNotificationToServer(notificationID) {
168831+ try {
168832+ const accountBaseURL = LoginService.getAccountBaseURL();
168833+ let url = `${accountBaseURL}/notificationAcknowledged`;
168834+
168835+ const requestBody = {
168836+ notificationID: notificationID
168837+ };
168838+
168839+ let fetchOptions = {
168840+ method: 'POST',
168841+ headers: {
168842+ 'Content-Type': 'application/json',
168843+ 'Accept': 'application/json'
168844+ },
168845+ body: JSON.stringify(requestBody)
168846+ };
168847+
168848+ // Handle different authentication methods for browser vs desktop
168849+ if (Phoenix.isNativeApp) {
168850+ // Desktop app: use appSessionID and validationCode
168851+ const profile = LoginService.getProfile();
168852+ if (profile && profile.apiKey && profile.validationCode) {
168853+ requestBody.appSessionID = profile.apiKey;
168854+ requestBody.validationCode = profile.validationCode;
168855+ fetchOptions.body = JSON.stringify(requestBody);
168856+ }
168857+ } else {
168858+ // Browser app: use session cookies
168859+ fetchOptions.credentials = 'include';
168860+ }
168861+
168862+ const response = await fetchFn(url, fetchOptions);
168863+
168864+ if (response.ok) {
168865+ const result = await response.json();
168866+ if (result.isSuccess) {
168867+ console.log(`Notification ${notificationID} acknowledged successfully`);
168868+ return true;
168869+ }
168870+ }
168871+
168872+ console.warn(`Failed to acknowledge notification ${notificationID}:`, response.status);
168873+ return false;
168874+ } catch (error) {
168875+ console.error(`Error acknowledging notification ${notificationID}:`, error);
168876+ return false;
168877+ }
168878+ }
168879+
168880+ /**
168881+ * Handle notification dismissal
168882+ * @param {string} notificationID - The notification ID that was dismissed
168883+ */
168884+ async function handleNotificationDismiss(notificationID) {
168885+ // Always mark as shown locally to prevent re-showing, even if API fails
168886+ markNotificationAsShown(notificationID);
168887+
168888+ // Call server API to acknowledge
168889+ return acknowledgeNotificationToServer(notificationID);
168890+ }
168891+
168892+ /**
168893+ * Check if a notification should be shown
168894+ * @param {Object} notification - The notification object from server
168895+ * @returns {boolean} - True if should be shown, false otherwise
168896+ */
168897+ function shouldShowNotification(notification) {
168898+ if (!notification || !notification.notificationID) {
168899+ return false;
168900+ }
168901+
168902+ // Check if expired
168903+ if (notification.validTill && Date.now() > notification.validTill) {
168904+ return false;
168905+ }
168906+
168907+ // Check if already shown (persistent storage)
168908+ const shownNotifications = getShownNotifications();
168909+ if (shownNotifications[notification.notificationID]) {
168910+ return false;
168911+ }
168912+
168913+ // Check if currently being shown (in-memory)
168914+ if (currentlyShownNotifications.has(notification.notificationID)) {
168915+ return false;
168916+ }
168917+
168918+ return true;
168919+ }
168920+
168921+ /**
168922+ * Display a single notification
168923+ * @param {Object} notification - The notification object from server
168924+ */
168925+ function displayNotification(notification) {
168926+ const {
168927+ notificationID,
168928+ title,
168929+ htmlContent,
168930+ options = {}
168931+ } = notification;
168932+
168933+ // Mark as currently showing to prevent duplicates
168934+ currentlyShownNotifications.add(notificationID);
168935+
168936+ // Prepare options for NotificationUI
168937+ const toastOptions = {
168938+ dismissOnClick: options.dismissOnClick !== undefined ? options.dismissOnClick : true,
168939+ toastStyle: options.toastStyle || NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.INFO
168940+ };
168941+
168942+ // Add autoCloseTimeS if provided
168943+ if (options.autoCloseTimeS) {
168944+ toastOptions.autoCloseTimeS = options.autoCloseTimeS;
168945+ }
168946+
168947+ // Create and show the toast notification
168948+ const notificationInstance = NotificationUI.createToastFromTemplate(
168949+ title,
168950+ htmlContent,
168951+ toastOptions
168952+ );
168953+
168954+ // Handle notification dismissal
168955+ notificationInstance.done(() => {
168956+ handleNotificationDismiss(notificationID);
168957+ });
168958+ }
168959+
168960+ /**
168961+ * Clean up stale notification IDs from state manager
168962+ * Removes notification IDs that are no longer in the remote notifications list
168963+ * @param {Array} remoteNotifications - The current notifications from server
168964+ */
168965+ function cleanupStaleNotifications(remoteNotifications) {
168966+ if (!remoteNotifications || remoteNotifications.length === 0) {
168967+ return;
168968+ }
168969+
168970+ // Build a set of remote notification IDs for quick lookup
168971+ const remoteIDs = new Set();
168972+ for (const notification of remoteNotifications) {
168973+ if (notification.notificationID) {
168974+ remoteIDs.add(notification.notificationID);
168975+ }
168976+ }
168977+
168978+ // Keep only notification IDs that are still in remote notifications
168979+ const shownNotifications = getShownNotifications();
168980+ const updatedShownNotifications = {};
168981+ for (const id in shownNotifications) {
168982+ if (remoteIDs.has(id)) {
168983+ updatedShownNotifications[id] = shownNotifications[id];
168984+ }
168985+ }
168986+
168987+ // Update state if we removed any stale IDs
168988+ const oldCount = Object.keys(shownNotifications).length;
168989+ const newCount = Object.keys(updatedShownNotifications).length;
168990+ if (newCount < oldCount) {
168991+ console.log(`Cleaning up ${oldCount - newCount} stale notification ID(s) from state`);
168992+ PreferencesManager.stateManager.set(PREF_NOTIFICATIONS_SHOWN_LIST, updatedShownNotifications);
168993+ }
168994+ }
168995+
168996+ /**
168997+ * Process notifications from entitlements
168998+ */
168999+ async function processNotifications() {
169000+ try {
169001+ const notifications = await EntitlementsManager.getNotifications();
169002+
169003+ if (!notifications || !Array.isArray(notifications)) {
169004+ return;
169005+ }
169006+
169007+ // Clean up stale notification IDs if we have at least 1 notification from server
169008+ if (notifications.length > 0) {
169009+ cleanupStaleNotifications(notifications);
169010+ }
169011+
169012+ // Filter and show new notifications
169013+ const notificationsToShow = notifications.filter(shouldShowNotification);
169014+
169015+ if (notificationsToShow.length > 0) {
169016+ console.log(`Showing ${notificationsToShow.length} new notification(s)`);
169017+ notificationsToShow.forEach(displayNotification);
169018+ }
169019+ } catch (error) {
169020+ console.error('Error processing notifications:', error);
169021+ }
169022+ }
169023+
169024+ /**
169025+ * Initialize the UserNotifications service
169026+ */
169027+ function init() {
169028+ EntitlementsManager = KernalModeTrust.EntitlementsManager;
169029+ LoginService = KernalModeTrust.loginService;
169030+
169031+ if (!EntitlementsManager || !LoginService) {
169032+ throw new Error("UserNotifications requires EntitlementsManager and LoginService in KernalModeTrust");
169033+ }
169034+
169035+ // Listen for entitlements changes
169036+ EntitlementsManager.on(EntitlementsManager.EVENT_ENTITLEMENTS_CHANGED, processNotifications);
169037+
169038+ console.log('UserNotifications service initialized');
169039+ }
169040+
169041+ // Test-only exports for integration testing
169042+ if (Phoenix.isTestWindow) {
169043+ window._test_user_notifications_exports = {
169044+ getShownNotifications,
169045+ markNotificationAsShown,
169046+ shouldShowNotification,
169047+ acknowledgeNotificationToServer,
169048+ processNotifications,
169049+ cleanupStaleNotifications,
169050+ currentlyShownNotifications,
169051+ setFetchFn: function (fn) {
169052+ fetchFn = fn;
169053+ }
169054+ };
169055+ }
169056+
169057+ exports.init = init;
169058+ // no public exports to prevent extension tampering
169059+ });
169060+
168726169061// SPDX-License-Identifier: AGPL-3.0-only
168727169062// Copyright (c) 2021 - present core.ai. All rights reserved.
168728169063
0 commit comments