ipprotection-content.mjs (9542B)
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 https://mozilla.org/MPL/2.0/. */ 4 5 import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; 6 import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; 7 import { 8 LINKS, 9 ERRORS, 10 } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; 11 12 // eslint-disable-next-line import/no-unassigned-import 13 import "chrome://browser/content/ipprotection/ipprotection-message-bar.mjs"; 14 // eslint-disable-next-line import/no-unassigned-import 15 import "chrome://browser/content/ipprotection/ipprotection-signedout.mjs"; 16 // eslint-disable-next-line import/no-unassigned-import 17 import "chrome://browser/content/ipprotection/ipprotection-status-card.mjs"; 18 // eslint-disable-next-line import/no-unassigned-import 19 import "chrome://global/content/elements/moz-toggle.mjs"; 20 21 /** 22 * Custom element that implements a message bar and status card for IP protection. 23 */ 24 export default class IPProtectionContentElement extends MozLitElement { 25 static queries = { 26 signedOutEl: "ipprotection-signedout", 27 messagebarEl: "ipprotection-message-bar", 28 statusCardEl: "ipprotection-status-card", 29 upgradeEl: "#upgrade-vpn-content", 30 activeSubscriptionEl: "#active-subscription-vpn-content", 31 supportLinkEl: "#vpn-support-link", 32 }; 33 34 static properties = { 35 state: { type: Object, attribute: false }, 36 _showMessageBar: { type: Boolean, state: true }, 37 _messageDismissed: { type: Boolean, state: true }, 38 }; 39 40 constructor() { 41 super(); 42 43 this.state = {}; 44 45 this.keyListener = this.#keyListener.bind(this); 46 this.messageBarListener = this.#messageBarListener.bind(this); 47 this.statusCardListener = this.#statusCardListener.bind(this); 48 this._showMessageBar = false; 49 this._messageDismissed = false; 50 } 51 52 connectedCallback() { 53 super.connectedCallback(); 54 this.dispatchEvent(new CustomEvent("IPProtection:Init", { bubbles: true })); 55 this.addEventListener("keydown", this.keyListener, { capture: true }); 56 this.addEventListener( 57 "ipprotection-status-card:user-toggled-on", 58 this.#statusCardListener 59 ); 60 this.addEventListener( 61 "ipprotection-status-card:user-toggled-off", 62 this.#statusCardListener 63 ); 64 this.addEventListener( 65 "ipprotection-site-settings-control:click", 66 this.#statusCardListener 67 ); 68 this.addEventListener( 69 "ipprotection-message-bar:user-dismissed", 70 this.#messageBarListener 71 ); 72 } 73 74 disconnectedCallback() { 75 super.disconnectedCallback(); 76 77 this.removeEventListener("keydown", this.keyListener, { capture: true }); 78 this.removeEventListener( 79 "ipprotection-status-card:user-toggled-on", 80 this.#statusCardListener 81 ); 82 this.removeEventListener( 83 "ipprotection-status-card:user-toggled-off", 84 this.#statusCardListener 85 ); 86 this.removeEventListener( 87 "ipprotection-site-settings-control:click", 88 this.#statusCardListener 89 ); 90 this.removeEventListener( 91 "ipprotection-message-bar:user-dismissed", 92 this.#messageBarListener 93 ); 94 } 95 96 get canEnableConnection() { 97 return this.state && this.state.isProtectionEnabled && !this.state.error; 98 } 99 100 get #hasErrors() { 101 return !this.state || this.state.error !== ""; 102 } 103 104 handleClickSupportLink(event) { 105 const win = event.target.ownerGlobal; 106 107 if (event.target === this.supportLinkEl) { 108 event.preventDefault(); 109 win.openWebLinkIn(LINKS.PRODUCT_URL, "tab"); 110 this.dispatchEvent( 111 new CustomEvent("IPProtection:Close", { bubbles: true }) 112 ); 113 } 114 } 115 116 handleUpgrade(event) { 117 const win = event.target.ownerGlobal; 118 win.openWebLinkIn(LINKS.PRODUCT_URL + "#pricing", "tab"); 119 // Close the panel 120 this.dispatchEvent( 121 new CustomEvent("IPProtection:ClickUpgrade", { bubbles: true }) 122 ); 123 124 Glean.ipprotection.clickUpgradeButton.record(); 125 } 126 127 focus() { 128 if (this.state.isSignedOut) { 129 this.signedOutEl?.focus(); 130 } else { 131 this.statusCardEl?.focus(); 132 } 133 } 134 135 #keyListener(event) { 136 let keyCode = event.code; 137 switch (keyCode) { 138 case "Tab": 139 case "ArrowUp": 140 // Intentional fall-through 141 case "ArrowDown": { 142 event.stopPropagation(); 143 event.preventDefault(); 144 145 let isForward = 146 (keyCode == "Tab" && !event.shiftKey) || keyCode == "ArrowDown"; 147 let direction = isForward 148 ? Services.focus.MOVEFOCUS_FORWARD 149 : Services.focus.MOVEFOCUS_BACKWARD; 150 Services.focus.moveFocus( 151 window, 152 null, 153 direction, 154 Services.focus.FLAG_BYKEY 155 ); 156 break; 157 } 158 } 159 } 160 161 #statusCardListener(event) { 162 if (event.type === "ipprotection-status-card:user-toggled-on") { 163 this.dispatchEvent( 164 new CustomEvent("IPProtection:UserEnable", { bubbles: true }) 165 ); 166 } else if (event.type === "ipprotection-status-card:user-toggled-off") { 167 this.dispatchEvent( 168 new CustomEvent("IPProtection:UserDisable", { bubbles: true }) 169 ); 170 } else if (event.type === "ipprotection-site-settings-control:click") { 171 this.dispatchEvent( 172 new CustomEvent("IPProtection:UserShowSiteSettings", { bubbles: true }) 173 ); 174 } 175 } 176 177 #messageBarListener(event) { 178 if (event.type === "ipprotection-message-bar:user-dismissed") { 179 this._showMessageBar = false; 180 this._messageDismissed = true; 181 this.state.error = ""; 182 this.state.bandwidthWarning = false; 183 } 184 } 185 186 updated(changedProperties) { 187 super.updated(changedProperties); 188 189 // Clear messages when there is an error. 190 if (this.state.error) { 191 this._messageDismissed = false; 192 } 193 } 194 195 messageBarTemplate() { 196 let messageId; 197 let messageLink; 198 let messageLinkl10nId; 199 let messageType = "info"; 200 // If there are errors, the error message should take precedence 201 if (this.#hasErrors) { 202 messageId = "ipprotection-message-generic-error"; 203 messageType = ERRORS.GENERIC; 204 } else if (this.state.bandwidthWarning) { 205 messageId = "ipprotection-message-bandwidth-warning"; 206 messageType = "warning"; 207 } else if (this.state.onboardingMessage) { 208 messageId = this.state.onboardingMessage; 209 messageType = "info"; 210 211 switch (this.state.onboardingMessage) { 212 case "ipprotection-message-continuous-onboarding-intro": 213 break; 214 case "ipprotection-message-continuous-onboarding-autostart": 215 messageLink = "about:settings#privacy"; 216 messageLinkl10nId = "setting-link"; 217 break; 218 case "ipprotection-message-continuous-onboarding-site-settings": 219 messageLink = "about:settings#privacy"; 220 messageLinkl10nId = "setting-link"; 221 break; 222 } 223 } 224 225 return html` 226 <ipprotection-message-bar 227 class="vpn-top-content" 228 type=${messageType} 229 .messageId=${ifDefined(messageId)} 230 .messageLink=${ifDefined(messageLink)} 231 .messageLinkl10nId=${ifDefined(messageLinkl10nId)} 232 ></ipprotection-message-bar> 233 `; 234 } 235 236 statusCardTemplate() { 237 // TODO: Pass site information to status-card to conditionally 238 // render the site settings control. (Bug 1997412) 239 return html` 240 <ipprotection-status-card 241 .protectionEnabled=${this.canEnableConnection} 242 .location=${this.state.location} 243 .siteData=${ifDefined(this.state.siteData)} 244 ></ipprotection-status-card> 245 `; 246 } 247 248 pausedTemplate() { 249 return html` 250 <div id="upgrade-vpn-content" class="vpn-bottom-content"> 251 <h2 252 id="upgrade-vpn-title" 253 data-l10n-id="upgrade-vpn-title" 254 class="vpn-subtitle" 255 ></h2> 256 <p 257 id="upgrade-vpn-paragraph" 258 data-l10n-id="upgrade-vpn-paragraph" 259 @click=${this.handleClickSupportLink} 260 > 261 <a 262 id="vpn-support-link" 263 href=${LINKS.PRODUCT_URL} 264 data-l10n-name="learn-more-vpn" 265 ></a> 266 </p> 267 <moz-button 268 id="upgrade-vpn-button" 269 class="vpn-button" 270 @click=${this.handleUpgrade} 271 type="secondary" 272 data-l10n-id="upgrade-vpn-button" 273 ></moz-button> 274 </div> 275 `; 276 } 277 278 mainContentTemplate() { 279 // TODO: Update support-page with new SUMO link for Mozilla VPN - Bug 1975474 280 if (this.state.isSignedOut) { 281 return html` <ipprotection-signedout></ipprotection-signedout> `; 282 } 283 284 if (this.state.paused) { 285 return html` ${this.pausedTemplate()} `; 286 } 287 288 return html` ${this.statusCardTemplate()} `; 289 } 290 291 render() { 292 if ( 293 (this.#hasErrors || 294 this.state.onboardingMessage || 295 this.state.bandwidthWarning) && 296 !this._messageDismissed 297 ) { 298 this._showMessageBar = true; 299 } 300 301 const messageBar = this._showMessageBar ? this.messageBarTemplate() : null; 302 303 let content = html`${messageBar}${this.mainContentTemplate()}`; 304 305 // TODO: Conditionally render post-upgrade subview within #ipprotection-content-wrapper - Bug 1973813 306 return html` 307 <link 308 rel="stylesheet" 309 href="chrome://browser/content/ipprotection/ipprotection-content.css" 310 /> 311 <div id="ipprotection-content-wrapper">${content}</div> 312 `; 313 } 314 } 315 316 customElements.define("ipprotection-content", IPProtectionContentElement);