diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index f67426009c3..d43e78eab06 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -28,6 +28,7 @@ + # Startup "preloaded" scripts that requre globals such as gBrowser and gURLBar diff --git a/src/zen/common/ZenPreloadedScripts.js b/src/zen/common/ZenPreloadedScripts.js index eaa87e51dd5..dcbb10561ba 100644 --- a/src/zen/common/ZenPreloadedScripts.js +++ b/src/zen/common/ZenPreloadedScripts.js @@ -12,6 +12,7 @@ "resource:///modules/zen/ZenSpaceManager.mjs", "chrome://browser/content/zen-components/ZenCompactMode.mjs", "chrome://browser/content/ZenUIManager.mjs", + "chrome://browser/content/zen-components/ZenTetraID.mjs", "chrome://browser/content/zen-components/ZenAISidebar.mjs", "chrome://browser/content/zen-components/ZenMods.mjs", "chrome://browser/content/zen-components/ZenKeyboardShortcuts.mjs", diff --git a/src/zen/common/jar.inc.mn b/src/zen/common/jar.inc.mn index ba436bf38bf..283ac0679ef 100644 --- a/src/zen/common/jar.inc.mn +++ b/src/zen/common/jar.inc.mn @@ -15,6 +15,7 @@ content/browser/zen-components/ZenHasPolyfill.mjs (../../zen/common/modules/ZenHasPolyfill.mjs) content/browser/zen-components/ZenSidebarNotification.mjs (../../zen/common/modules/ZenSidebarNotification.mjs) content/browser/zen-components/ZenMenubar.mjs (../../zen/common/modules/ZenMenubar.mjs) + content/browser/zen-components/ZenTetraID.mjs (../../zen/common/modules/ZenTetraID.mjs) content/browser/zen-components/ZenEmojisData.min.mjs (../../zen/common/emojis/ZenEmojisData.min.mjs) content/browser/zen-components/ZenEmojiPicker.mjs (../../zen/common/emojis/ZenEmojiPicker.mjs) @@ -33,6 +34,7 @@ content/browser/zen-styles/zen-branding.css (../../zen/common/styles/zen-branding.css) content/browser/zen-styles/zen-sidebar-notification.css (../../zen/common/styles/zen-sidebar-notification.css) content/browser/zen-styles/zen-ai-sidebar.css (../../zen/common/styles/zen-ai-sidebar.css) + content/browser/zen-styles/zen-tetraid.css (../../zen/common/styles/zen-tetraid.css) content/browser/zen-styles/zen-panels/bookmarks.css (../../zen/common/styles/zen-panels/bookmarks.css) content/browser/zen-styles/zen-panels/print.css (../../zen/common/styles/zen-panels/print.css) diff --git a/src/zen/common/modules/ZenTetraID.mjs b/src/zen/common/modules/ZenTetraID.mjs new file mode 100644 index 00000000000..e511a559d08 --- /dev/null +++ b/src/zen/common/modules/ZenTetraID.mjs @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TETRAID_BASE_URL_PREF = "identity.tetraid.auth.base_url"; +const TETRAID_DEFAULT_BASE_URL = "https://tetraid-altzen-auth.vercel.app"; +const TETRAID_PLACEHOLDER_STATE_PREF = "identity.tetraid.placeholder_state"; +const TETRAID_PLACEHOLDER_DISPLAY_NAME_PREF = + "identity.tetraid.placeholder_display_name"; +const TETRAID_PLACEHOLDER_USERNAME_PREF = + "identity.tetraid.placeholder_username"; + +const ACCOUNT_STATUS_ID = "appMenu-fxa-status2"; +const ACCOUNT_TEXT_ID = "appMenu-fxa-text"; +const ACCOUNT_ACTION_ID = "appMenu-fxa-label2"; +const ACCOUNT_TITLE_ID = "appMenu-header-title"; +const ACCOUNT_DESCRIPTION_ID = "appMenu-header-description"; +const LEGACY_TETRAID_STATUS_ID = "appMenu-tetraid-status"; + +let gTetraIDServices; +let gTetraIDServicesResolved = false; + +function warnTetraID(message, error) { + console.warn(`TetraID: ${message}`, error); +} + +function getTetraIDServices() { + if (gTetraIDServicesResolved) { + return gTetraIDServices; + } + + gTetraIDServicesResolved = true; + gTetraIDServices = globalThis.Services || globalThis.window?.Services; + + if (!gTetraIDServices && globalThis.ChromeUtils?.importESModule) { + try { + gTetraIDServices = ChromeUtils.importESModule( + "resource://gre/modules/Services.sys.mjs" + ).Services; + } catch (error) { + warnTetraID( + "Services unavailable; account UI will use safe defaults.", + error + ); + } + } + + return gTetraIDServices; +} + +class ZenTetraIDStateAdapter { + get state() { + const services = getTetraIDServices(); + if (!services?.prefs) { + return { status: "signedOut" }; + } + + if ( + services.prefs.getStringPref( + TETRAID_PLACEHOLDER_STATE_PREF, + "signedOut" + ) == "signedIn" + ) { + return { + status: "signedIn", + displayName: services.prefs.getStringPref( + TETRAID_PLACEHOLDER_DISPLAY_NAME_PREF, + "AltZen" + ), + username: services.prefs.getStringPref( + TETRAID_PLACEHOLDER_USERNAME_PREF, + "@altzen" + ), + }; + } + + return { status: "signedOut" }; + } + + async refresh() { + return this.state; + } +} + +class ZenTetraIDAccountMenu { + #stateAdapter = new ZenTetraIDStateAdapter(); + #lastState = { status: "signedOut" }; + + init() { + document.addEventListener("popupshowing", event => { + if (event.target.id == "appMenu-popup") { + this.refreshTetraIDAccountState(); + window.setTimeout(() => this.refreshTetraIDAccountState(), 0); + } + }); + + document.addEventListener("ViewShowing", event => { + if (event.target.id == "appMenu-mainView") { + this.refreshTetraIDAccountState(); + window.setTimeout(() => this.refreshTetraIDAccountState(), 0); + } + }); + + const handleAccountActivation = event => this.handleAccountActivation(event); + document.addEventListener("command", handleAccountActivation, true); + document.addEventListener("click", handleAccountActivation, true); + } + + get baseUrl() { + const services = getTetraIDServices(); + if (!services?.prefs) { + return TETRAID_DEFAULT_BASE_URL; + } + + return services.prefs + .getStringPref(TETRAID_BASE_URL_PREF, TETRAID_DEFAULT_BASE_URL) + .replace(/\/$/, ""); + } + + async refreshTetraIDAccountState() { + try { + this.#lastState = await this.#stateAdapter.refresh(); + } catch (error) { + warnTetraID("Account state refresh failed; using signed-out state.", error); + this.#lastState = { status: "signedOut" }; + } + + this.renderMenuRow(this.#lastState); + return this.#lastState; + } + + renderMenuRow(state = this.#lastState) { + const legacyTetraIDStatus = document.getElementById( + LEGACY_TETRAID_STATUS_ID + ); + if (legacyTetraIDStatus) { + legacyTetraIDStatus.hidden = true; + } + + const statusItem = document.getElementById(ACCOUNT_STATUS_ID); + const text = document.getElementById(ACCOUNT_TEXT_ID); + const actionButton = document.getElementById(ACCOUNT_ACTION_ID); + const title = document.getElementById(ACCOUNT_TITLE_ID); + const description = document.getElementById(ACCOUNT_DESCRIPTION_ID); + + if (!statusItem || !text || !actionButton || !title || !description) { + return; + } + + statusItem.hidden = false; + statusItem.setAttribute("tetraid-entry", "true"); + statusItem.classList.add("toolbaritem-combined-buttons"); + statusItem.removeAttribute("fxastatus"); + statusItem.removeAttribute("tooltiptext"); + statusItem.removeAttribute("data-l10n-id"); + statusItem.removeAttribute("command"); + statusItem.removeAttribute("oncommand"); + + text.removeAttribute("data-l10n-id"); + text.hidden = false; + actionButton.removeAttribute("data-l10n-id"); + actionButton.removeAttribute("command"); + actionButton.removeAttribute("oncommand"); + actionButton.removeAttribute("key"); + actionButton.removeAttribute("aria-labelledby"); + actionButton.classList.remove("subviewbutton-nav"); + actionButton.classList.remove("subviewbutton-iconic"); + actionButton.classList.add("subviewbutton-iconic", "tetraid-chevron-button"); + actionButton.setAttribute("closemenu", "none"); + actionButton.hidden = false; + actionButton.setAttribute("label", ""); + actionButton.setAttribute( + "image", + "chrome://global/skin/icons/arrow-right.svg" + ); + + title.hidden = true; + description.hidden = true; + + if (state.status == "signedIn") { + statusItem.setAttribute("tetraid-status", "signed-in"); + text.replaceChildren(this.#buildSignedInIdentity(state)); + actionButton.setAttribute("aria-label", "Open TetraID account"); + actionButton.setAttribute("title", "Open TetraID account"); + actionButton.setAttribute("tooltiptext", "Open TetraID account"); + return; + } + + statusItem.setAttribute("tetraid-status", "signed-out"); + text.textContent = "Connect AltZen to your TetraID"; + actionButton.setAttribute("aria-label", "Sign in to TetraID"); + actionButton.setAttribute("title", "Sign in to TetraID"); + actionButton.setAttribute("tooltiptext", "Sign in to TetraID"); + } + + showSignInDialog() { + const services = getTetraIDServices(); + if (!services?.prompt) { + warnTetraID("Prompt service unavailable; sign-in dialog was not opened."); + return; + } + + const buttonFlags = + services.prompt.BUTTON_POS_0 * services.prompt.BUTTON_TITLE_IS_STRING + + services.prompt.BUTTON_POS_1 * services.prompt.BUTTON_TITLE_CANCEL; + const buttonPressed = services.prompt.confirmEx( + window, + "Sign in to TetraID", + "Use your TetraID to connect AltZen with your Tetra account.", + buttonFlags, + "Continue", + null, + null, + null, + {} + ); + + if (buttonPressed == 0) { + this.openAuthStart(); + } + } + + openAuthStart() { + this.openTetraIDAuthFlow(); + } + + openTetraIDAuthFlow() { + window.openTrustedLinkIn(`${this.baseUrl}/api/auth/login`, "tab"); + } + + openAccountHome() { + window.openTrustedLinkIn(`${this.baseUrl}/`, "tab"); + } + + #buildSignedInIdentity(state) { + const container = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "div" + ); + container.id = "tetraid-menu-identity"; + + const avatar = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "span" + ); + avatar.id = "tetraid-menu-avatar"; + avatar.setAttribute("aria-hidden", "true"); + + const copy = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "span" + ); + copy.id = "tetraid-menu-copy"; + + const name = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "span" + ); + name.id = "tetraid-menu-name"; + name.textContent = state.displayName || "AltZen"; + + const username = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "span" + ); + username.id = "tetraid-menu-username"; + username.textContent = state.username || "@altzen"; + + copy.append(name, username); + container.append(avatar, copy); + return container; + } + + handleAccountActivation(event) { + const target = event.target?.closest?.( + `#${ACCOUNT_ACTION_ID}, #${ACCOUNT_STATUS_ID}` + ); + if (!target) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + if (this.#lastState.status == "signedIn") { + this.openAccountHome(); + } else { + this.openTetraIDAuthFlow(); + } + } +} + +try { + window.gZenTetraID = new ZenTetraIDAccountMenu(); + window.refreshTetraIDAccountState = () => + window.gZenTetraID.refreshTetraIDAccountState(); + window.gZenTetraID.init(); +} catch (error) { + warnTetraID("Initialization failed; browser chrome will continue.", error); +} diff --git a/src/zen/common/styles/zen-tetraid.css b/src/zen/common/styles/zen-tetraid.css new file mode 100644 index 00000000000..f33ba2980b4 --- /dev/null +++ b/src/zen/common/styles/zen-tetraid.css @@ -0,0 +1,97 @@ +#appMenu-fxa-status2[tetraid-entry="true"] { + gap: 8px; + min-height: 48px; + padding-block: 4px; +} + +#appMenu-fxa-status2[tetraid-entry="true"] > #appMenu-fxa-text { + flex: 1; + min-width: 0; + font-weight: 600; + line-height: 1.25; +} + +#appMenu-fxa-status2[tetraid-entry="true"] > #appMenu-fxa-label2 { + flex: 0 0 auto; +} + +#appMenu-fxa-status2[tetraid-entry="true"] > #appMenu-fxa-label2.tetraid-chevron-button { + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + fill-opacity: 0.75; + list-style-image: url("chrome://global/skin/icons/arrow-right.svg"); + appearance: none; + justify-content: center; + min-width: 28px; + min-height: 28px; + margin-inline-start: 4px; + padding: 5px; +} + +#appMenu-fxa-status2[tetraid-entry="true"] > #appMenu-fxa-label2.tetraid-chevron-button > .toolbarbutton-text { + display: none; +} + +#appMenu-fxa-status2[tetraid-entry="true"] > #appMenu-fxa-label2.tetraid-chevron-button > .toolbarbutton-icon { + display: -moz-box; + width: 16px; + height: 16px; + margin: 0; +} + +#appMenu-fxa-status2[tetraid-entry="true"] > #appMenu-fxa-label2.tetraid-chevron-button:-moz-locale-dir(rtl) { + list-style-image: url("chrome://global/skin/icons/arrow-left.svg"); +} + +#appMenu-fxa-status2[tetraid-status="signed-in"] { + align-items: center; + min-height: 66px; + padding-block: 6px; +} + +#appMenu-fxa-status2[tetraid-status="signed-in"] > #appMenu-fxa-label2 { + margin-inline-start: 8px; + white-space: nowrap; +} + +#tetraid-menu-identity { + display: flex; + align-items: center; + min-width: 0; + gap: 12px; +} + +#tetraid-menu-avatar { + width: 38px; + height: 38px; + flex: 0 0 auto; + border-radius: 50%; + background: + radial-gradient(circle at 35% 35%, #ffffffcc 0 18%, transparent 19%), + linear-gradient(145deg, #f56d41, #55a5ff); +} + +#tetraid-menu-copy { + display: flex; + flex-direction: column; + min-width: 0; + gap: 3px; +} + +#tetraid-menu-name, +#tetraid-menu-username { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#tetraid-menu-name { + font-weight: 600; + line-height: 1.2; +} + +#tetraid-menu-username { + color: color-mix(in srgb, currentColor 68%, transparent); + font-size: 0.92em; + font-weight: 400; +}