IPProtectionPanel.sys.mjs (13424B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 CustomizableUI: 9 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 10 IPPEnrollAndEntitleManager: 11 "moz-src:///browser/components/ipprotection/IPPEnrollAndEntitleManager.sys.mjs", 12 IPPProxyManager: 13 "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs", 14 IPPProxyStates: 15 "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs", 16 IPProtectionService: 17 "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs", 18 IPProtection: 19 "moz-src:///browser/components/ipprotection/IPProtection.sys.mjs", 20 IPPSignInWatcher: 21 "moz-src:///browser/components/ipprotection/IPPSignInWatcher.sys.mjs", 22 }); 23 24 import { 25 LINKS, 26 ERRORS, 27 } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; 28 29 let hasCustomElements = new WeakSet(); 30 31 /** 32 * Manages updates for a IP Protection panelView in a given browser window. 33 */ 34 export class IPProtectionPanel { 35 static CONTENT_TAGNAME = "ipprotection-content"; 36 static CUSTOM_ELEMENTS_SCRIPT = 37 "chrome://browser/content/ipprotection/ipprotection-customelements.js"; 38 static WIDGET_ID = "ipprotection-button"; 39 static PANEL_ID = "PanelUI-ipprotection"; 40 static TITLE_L10N_ID = "ipprotection-title"; 41 static HEADER_AREA_ID = "PanelUI-ipprotection-header"; 42 static CONTENT_AREA_ID = "PanelUI-ipprotection-content"; 43 static HEADER_BUTTON_ID = "ipprotection-header-button"; 44 45 /** 46 * Loads the ipprotection custom element script 47 * into a given window. 48 * 49 * Called on IPProtection.init for a new browser window. 50 * 51 * @param {Window} window 52 */ 53 static loadCustomElements(window) { 54 if (hasCustomElements.has(window)) { 55 // Don't add the elements again for the same window. 56 return; 57 } 58 Services.scriptloader.loadSubScriptWithOptions( 59 IPProtectionPanel.CUSTOM_ELEMENTS_SCRIPT, 60 { 61 target: window, 62 async: true, 63 } 64 ); 65 hasCustomElements.add(window); 66 } 67 68 /** 69 * @typedef {object} State 70 * @property {boolean} isProtectionEnabled 71 * The timestamp in milliseconds since IP Protection was enabled 72 * @property {boolean} isSignedOut 73 * True if not signed in to account 74 * @property {object} location 75 * Data about the server location the proxy is connected to 76 * @property {string} location.name 77 * The location country name 78 * @property {string} location.code 79 * The location country code 80 * @property {"generic" | ""} error 81 * The error type as a string if an error occurred, or empty string if there are no errors. 82 * @property {boolean} isAlpha 83 * True if we're running the Alpha variant, else false. 84 * @property {boolean} hasUpgraded 85 * True if a Mozilla VPN subscription is linked to the user's Mozilla account. 86 * @property {string} onboardingMessage 87 * Continuous onboarding message to display in-panel, empty string if none applicable 88 * @property {boolean} paused 89 * True if the VPN service has been paused due to bandwidth limits 90 */ 91 92 /** 93 * @type {State} 94 */ 95 state = {}; 96 panel = null; 97 initiatedUpgrade = false; 98 99 /** 100 * Check the state of the enclosing panel to see if 101 * it is active (open or showing). 102 */ 103 get active() { 104 let panelParent = this.panel?.closest("panel"); 105 if (!panelParent) { 106 return false; 107 } 108 return panelParent.state == "open" || panelParent.state == "showing"; 109 } 110 111 /** 112 * Creates an instance of IPProtectionPanel for a specific browser window. 113 * 114 * Inserts the panel component customElements registry script. 115 * 116 * @param {Window} window 117 * Window containing the panelView to manage. 118 */ 119 constructor(window) { 120 this.handleEvent = this.#handleEvent.bind(this); 121 122 this.state = { 123 isSignedOut: !lazy.IPPSignInWatcher.isSignedIn, 124 isProtectionEnabled: 125 lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE, 126 location: { 127 name: "United States", 128 code: "us", 129 }, 130 error: "", 131 isAlpha: lazy.IPPEnrollAndEntitleManager.isAlpha, 132 hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded, 133 onboardingMessage: "", 134 bandwidthWarning: "", 135 paused: false, 136 }; 137 138 if (window) { 139 IPProtectionPanel.loadCustomElements(window); 140 } 141 142 this.#addProxyListeners(); 143 } 144 145 /** 146 * Set the state for this panel. 147 * 148 * Updates the current panel component state, 149 * if the panel is currently active (showing or not hiding). 150 * 151 * @example 152 * panel.setState({ 153 * isSomething: true, 154 * }); 155 * 156 * @param {object} state 157 * The state object from IPProtectionPanel. 158 */ 159 setState(state) { 160 Object.assign(this.state, state); 161 162 if (this.active) { 163 this.updateState(); 164 } 165 } 166 167 /** 168 * Updates the state of the panel component. 169 * 170 * @param {object} state 171 * The state object from IPProtectionPanel. 172 * @param {Element} panelEl 173 * The panelEl element to update the state on. 174 */ 175 updateState(state = this.state, panelEl = this.panel) { 176 if (!panelEl?.isConnected || !panelEl.state) { 177 return; 178 } 179 180 panelEl.state = state; 181 panelEl.requestUpdate(); 182 } 183 184 #startProxy() { 185 lazy.IPPProxyManager.start(); 186 } 187 188 #stopProxy() { 189 lazy.IPPProxyManager.stop(); 190 } 191 192 /** 193 * Opens the help page in a new tab and closes the panel. 194 * 195 * @param {Event} e 196 */ 197 static showHelpPage(e) { 198 let win = e.target?.ownerGlobal; 199 if (win) { 200 win.openWebLinkIn(LINKS.SUPPORT_URL, "tab"); 201 } 202 203 let panelParent = e.target?.closest("panel"); 204 if (panelParent) { 205 panelParent.hidePopup(); 206 } 207 } 208 209 /** 210 * Updates the visibility of the panel components before they will shown. 211 * 212 * - If the panel component has already been created, updates the state. 213 * - Creates a panel component if need, state will be updated on once it has 214 * been connected. 215 * 216 * @param {XULElement} panelView 217 * The panelView element from the CustomizableUI widget callback. 218 */ 219 showing(panelView) { 220 if (this.initiatedUpgrade) { 221 lazy.IPPEnrollAndEntitleManager.refetchEntitlement(); 222 this.initiatedUpgrade = false; 223 } 224 225 if (this.panel) { 226 this.updateState(); 227 } else { 228 this.#createPanel(panelView); 229 } 230 231 // TODO: Stop counting after all onboarding messages have been shown - Bug 1997332 232 let currentCount = Services.prefs.getIntPref( 233 "browser.ipProtection.panelOpenCount" 234 ); 235 let updatedCount = currentCount + 1; 236 Services.prefs.setIntPref( 237 "browser.ipProtection.panelOpenCount", 238 updatedCount 239 ); 240 } 241 242 /** 243 * Called when the panel elements will be hidden. 244 * 245 * Disables updates to the panel. 246 */ 247 hiding() { 248 this.destroy(); 249 } 250 251 /** 252 * Creates a panel component in a panelView. 253 * 254 * @param {MozBrowser} panelView 255 */ 256 #createPanel(panelView) { 257 let { ownerDocument } = panelView; 258 259 let headerArea = panelView.querySelector( 260 `#${IPProtectionPanel.HEADER_AREA_ID}` 261 ); 262 let headerButton = headerArea.querySelector( 263 `#${IPProtectionPanel.HEADER_BUTTON_ID}` 264 ); 265 if (!headerButton) { 266 headerButton = this.#createHeaderButton(ownerDocument); 267 headerArea.appendChild(headerButton); 268 } 269 // Reset the tab index to ensure it is focusable. 270 headerButton.setAttribute("tabindex", "0"); 271 272 let contentEl = ownerDocument.createElement( 273 IPProtectionPanel.CONTENT_TAGNAME 274 ); 275 this.panel = contentEl; 276 277 contentEl.dataset.capturesFocus = "true"; 278 279 this.#addPanelListeners(ownerDocument); 280 281 let contentArea = panelView.querySelector( 282 `#${IPProtectionPanel.CONTENT_AREA_ID}` 283 ); 284 contentArea.appendChild(contentEl); 285 } 286 287 #createHeaderButton(ownerDocument) { 288 const headerButton = ownerDocument.createXULElement("toolbarbutton"); 289 290 headerButton.id = IPProtectionPanel.HEADER_BUTTON_ID; 291 headerButton.className = "panel-info-button"; 292 headerButton.dataset.capturesFocus = "true"; 293 294 ownerDocument.l10n.setAttributes(headerButton, "ipprotection-help-button"); 295 headerButton.addEventListener("click", IPProtectionPanel.showHelpPage); 296 headerButton.addEventListener("keypress", e => { 297 if (e.code == "Space" || e.code == "Enter") { 298 IPProtectionPanel.showHelpPage(e); 299 } 300 }); 301 return headerButton; 302 } 303 304 /** 305 * Open the IP Protection panel in the given window. 306 * 307 * @param {Window} window - which window to open the panel in. 308 * @returns {Promise<void>} 309 */ 310 async open(window) { 311 if (!lazy.IPProtection.created || !window?.PanelUI) { 312 return; 313 } 314 315 let widget = lazy.CustomizableUI.getWidget(IPProtectionPanel.WIDGET_ID); 316 let anchor = widget.forWindow(window).anchor; 317 await window.PanelUI.showSubView(IPProtectionPanel.PANEL_ID, anchor); 318 } 319 320 /** 321 * Close the containing panel popup. 322 */ 323 close() { 324 let panelParent = this.panel?.closest("panel"); 325 if (!panelParent) { 326 return; 327 } 328 panelParent.hidePopup(); 329 } 330 331 /** 332 * Start flow for signing in and then opening the panel on success 333 */ 334 async startLoginFlow() { 335 let window = this.panel.ownerGlobal; 336 let browser = window.gBrowser; 337 this.close(); 338 let isSignedIn = await lazy.IPProtectionService.startLoginFlow(browser); 339 if (isSignedIn) { 340 await this.open(window); 341 } 342 } 343 344 /** 345 * Remove added elements and listeners. 346 */ 347 destroy() { 348 if (this.panel) { 349 this.panel.remove(); 350 this.#removePanelListeners(this.panel.ownerDocument); 351 this.panel = null; 352 if (this.state.error) { 353 this.setState({ 354 error: "", 355 }); 356 } 357 } 358 } 359 360 uninit() { 361 this.destroy(); 362 this.#removeProxyListeners(); 363 } 364 365 #addPanelListeners(doc) { 366 doc.addEventListener("IPProtection:Init", this.handleEvent); 367 doc.addEventListener("IPProtection:ClickUpgrade", this.handleEvent); 368 doc.addEventListener("IPProtection:Close", this.handleEvent); 369 doc.addEventListener("IPProtection:UserEnable", this.handleEvent); 370 doc.addEventListener("IPProtection:UserDisable", this.handleEvent); 371 doc.addEventListener("IPProtection:SignIn", this.handleEvent); 372 doc.addEventListener("IPProtection:UserShowSiteSettings", this.handleEvent); 373 } 374 375 #removePanelListeners(doc) { 376 doc.removeEventListener("IPProtection:Init", this.handleEvent); 377 doc.removeEventListener("IPProtection:ClickUpgrade", this.handleEvent); 378 doc.removeEventListener("IPProtection:Close", this.handleEvent); 379 doc.removeEventListener("IPProtection:UserEnable", this.handleEvent); 380 doc.removeEventListener("IPProtection:UserDisable", this.handleEvent); 381 doc.removeEventListener("IPProtection:SignIn", this.handleEvent); 382 doc.removeEventListener( 383 "IPProtection:UserShowSiteSettings", 384 this.handleEvent 385 ); 386 } 387 388 #addProxyListeners() { 389 lazy.IPProtectionService.addEventListener( 390 "IPProtectionService:StateChanged", 391 this.handleEvent 392 ); 393 lazy.IPPProxyManager.addEventListener( 394 "IPPProxyManager:StateChanged", 395 this.handleEvent 396 ); 397 lazy.IPPEnrollAndEntitleManager.addEventListener( 398 "IPPEnrollAndEntitleManager:StateChanged", 399 this.handleEvent 400 ); 401 } 402 403 #removeProxyListeners() { 404 lazy.IPPEnrollAndEntitleManager.removeEventListener( 405 "IPPEnrollAndEntitleManager:StateChanged", 406 this.handleEvent 407 ); 408 lazy.IPPProxyManager.removeEventListener( 409 "IPPProxyManager:StateChanged", 410 this.handleEvent 411 ); 412 lazy.IPProtectionService.removeEventListener( 413 "IPProtectionService:StateChanged", 414 this.handleEvent 415 ); 416 } 417 418 #handleEvent(event) { 419 if (event.type == "IPProtection:Init") { 420 this.updateState(); 421 } else if (event.type == "IPProtection:Close") { 422 this.close(); 423 } else if (event.type == "IPProtection:UserEnable") { 424 this.#startProxy(); 425 Services.prefs.setBoolPref("browser.ipProtection.userEnabled", true); 426 } else if (event.type == "IPProtection:UserDisable") { 427 this.#stopProxy(); 428 Services.prefs.setBoolPref("browser.ipProtection.userEnabled", false); 429 } else if (event.type == "IPProtection:ClickUpgrade") { 430 // Let the service know that we tried upgrading at least once 431 this.initiatedUpgrade = true; 432 this.close(); 433 } else if (event.type == "IPProtection:SignIn") { 434 this.startLoginFlow(); 435 } else if ( 436 event.type == "IPPProxyManager:StateChanged" || 437 event.type == "IPProtectionService:StateChanged" || 438 event.type === "IPPEnrollAndEntitleManager:StateChanged" 439 ) { 440 let hasError = 441 lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR && 442 lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC); 443 444 this.setState({ 445 isSignedOut: !lazy.IPPSignInWatcher.isSignedIn, 446 isProtectionEnabled: 447 lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE, 448 hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded, 449 error: hasError ? ERRORS.GENERIC : "", 450 }); 451 } else if (event.type == "IPProtection:UserShowSiteSettings") { 452 // TODO: show subview for site settings (Bug 1997413) 453 } 454 } 455 }