turn-on-scheduled-backups.mjs (15710B)
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 import { html, nothing } from "chrome://global/content/vendor/lit.all.mjs"; 6 import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; 7 8 // eslint-disable-next-line import/no-unassigned-import 9 import "chrome://global/content/elements/moz-message-bar.mjs"; 10 // eslint-disable-next-line import/no-unassigned-import 11 import "chrome://browser/content/backup/password-validation-inputs.mjs"; 12 13 import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs"; 14 15 const ENABLE_ERROR_L10N_IDS = Object.freeze({ 16 [ERRORS.FILE_SYSTEM_ERROR]: "turn-on-scheduled-backups-error-file-system", 17 [ERRORS.INVALID_PASSWORD]: "backup-error-password-requirements", 18 [ERRORS.UNKNOWN]: "backup-error-retry", 19 }); 20 21 /** 22 * @param {number} errorCode Error code from backup-constants.mjs 23 * @returns {string} Localization ID for error message 24 */ 25 function getEnableErrorL10nId(errorCode) { 26 return ( 27 ENABLE_ERROR_L10N_IDS[errorCode] ?? ENABLE_ERROR_L10N_IDS[ERRORS.UNKNOWN] 28 ); 29 } 30 31 /** 32 * The widget for showing available options when users want to turn on 33 * scheduled backups. 34 */ 35 export default class TurnOnScheduledBackups extends MozLitElement { 36 #placeholderIconURL = "chrome://global/skin/icons/page-portrait.svg"; 37 38 static properties = { 39 backupServiceState: { type: Object }, 40 // passed in from parents 41 defaultIconURL: { type: String, reflect: true }, 42 defaultLabel: { type: String, reflect: true }, 43 defaultPath: { type: String, reflect: true }, 44 supportBaseLink: { type: String }, 45 embeddedFxBackupOptIn: { 46 type: Boolean, 47 reflect: true, 48 attribute: "embedded-fx-backup-opt-in", 49 }, 50 hideFilePathChooser: { 51 type: Boolean, 52 reflect: true, 53 attribute: "hide-file-path-chooser", 54 }, 55 hideSecondaryButton: { 56 type: Boolean, 57 reflect: true, 58 attribute: "hide-secondary-button", 59 }, 60 backupIsEncrypted: { 61 type: Boolean, 62 reflect: true, 63 attribute: "backup-is-encrypted", 64 }, 65 filePathLabelL10nId: { 66 type: String, 67 reflect: true, 68 attribute: "file-path-label-l10n-id", 69 }, 70 turnOnBackupHeaderL10nId: { 71 type: String, 72 reflect: true, 73 attribute: "turn-on-backup-header-l10n-id", 74 }, 75 createPasswordLabelL10nId: { 76 type: String, 77 reflect: true, 78 attribute: "create-password-label-l10n-id", 79 }, 80 turnOnBackupConfirmBtnL10nId: { 81 type: String, 82 reflect: true, 83 attribute: "turn-on-backup-confirm-btn-l10n-id", 84 }, 85 turnOnBackupCancelBtnL10nId: { 86 type: String, 87 reflect: true, 88 attribute: "turn-on-backup-cancel-btn-l10n-id", 89 }, 90 91 // internal state 92 _newIconURL: { type: String, state: true }, 93 _newLabel: { type: String, state: true }, 94 _newPath: { type: String, state: true }, 95 _showPasswordOptions: { type: Boolean, reflect: true, state: true }, 96 _passwordsMatch: { type: Boolean, state: true }, 97 _inputPassValue: { type: String, state: true }, 98 99 // managed by BackupUIChild 100 enableBackupErrorCode: { type: Number }, 101 }; 102 103 static get queries() { 104 return { 105 cancelButtonEl: "#backup-turn-on-scheduled-cancel-button", 106 confirmButtonEl: "#backup-turn-on-scheduled-confirm-button", 107 filePathButtonEl: "#backup-location-filepicker-button", 108 filePathInputCustomEl: "#backup-location-filepicker-input-custom", 109 filePathInputDefaultEl: "#backup-location-filepicker-input-default", 110 passwordOptionsCheckboxEl: "#sensitive-data-checkbox-input", 111 passwordOptionsExpandedEl: "#passwords", 112 errorEl: "#enable-backup-encryption-error", 113 }; 114 } 115 116 constructor() { 117 super(); 118 this.backupServiceState = {}; 119 this.defaultIconURL = ""; 120 this.defaultLabel = ""; 121 this.defaultPath = ""; 122 this._newIconURL = ""; 123 this._newLabel = ""; 124 this._newPath = ""; 125 this._showPasswordOptions = false; 126 this._passwordsMatch = false; 127 this.enableBackupErrorCode = 0; 128 this.disableSubmit = false; 129 } 130 131 connectedCallback() { 132 super.connectedCallback(); 133 this.dispatchEvent( 134 new CustomEvent("BackupUI:InitWidget", { bubbles: true }) 135 ); 136 137 // listen to events from BackupUIChild 138 this.addEventListener("BackupUI:SelectNewFilepickerPath", this); 139 140 // listen to events from <password-validation-inputs> 141 this.addEventListener("ValidPasswordsDetected", this); 142 this.addEventListener("InvalidPasswordsDetected", this); 143 144 // listens to keydown events 145 this.addEventListener("keydown", this); 146 } 147 148 handleEvent(event) { 149 if (event.type == "BackupUI:SelectNewFilepickerPath") { 150 let { path, filename, iconURL } = event.detail; 151 this._newPath = path; 152 this._newLabel = filename; 153 this._newIconURL = iconURL; 154 155 if (this.embeddedFxBackupOptIn) { 156 // Let's set a persistent path 157 this.dispatchEvent( 158 new CustomEvent("BackupUI:SetEmbeddedComponentPersistentData", { 159 bubbles: true, 160 detail: { 161 path, 162 label: filename, 163 iconURL, 164 }, 165 }) 166 ); 167 } 168 } else if (event.type == "ValidPasswordsDetected") { 169 let { password } = event.detail; 170 this._passwordsMatch = true; 171 this._inputPassValue = password; 172 } else if (event.type == "InvalidPasswordsDetected") { 173 this._passwordsMatch = false; 174 this._inputPassValue = ""; 175 } else if (event.type == "keydown") { 176 if ( 177 event.key === "Enter" && 178 (event.originalTarget.id == 179 "backup-location-filepicker-input-default" || 180 event.originalTarget.id == "backup-location-filepicker-input-custom") 181 ) { 182 event.preventDefault(); 183 } 184 } 185 } 186 187 async handleChooseLocation() { 188 this.dispatchEvent( 189 new CustomEvent("BackupUI:ShowFilepicker", { 190 bubbles: true, 191 detail: { 192 win: window.browsingContext, 193 }, 194 }) 195 ); 196 } 197 198 close() { 199 this.dispatchEvent( 200 new CustomEvent("dialogCancel", { 201 bubbles: true, 202 composed: true, 203 }) 204 ); 205 } 206 207 handleConfirm() { 208 let detail = { 209 parentDirPath: this._newPath || this.defaultPath, 210 }; 211 212 if (this._showPasswordOptions && this._passwordsMatch) { 213 detail.password = this._inputPassValue; 214 } 215 216 if (this.embeddedFxBackupOptIn && this.backupIsEncrypted) { 217 if (!detail.password) { 218 // We're in the embedded component and we haven't set a password yet 219 // when one is expected, let's not do a confirm action yet! 220 this.dispatchEvent( 221 new CustomEvent("SpotlightOnboardingAdvanceScreens", { 222 bubbles: true, 223 }) 224 ); 225 return; 226 } 227 228 // The persistent data will take precedence over the default path 229 detail.parentDirPath = 230 this.backupServiceState?.embeddedComponentPersistentData?.path || 231 detail.parentDirPath; 232 } 233 234 this.dispatchEvent( 235 new CustomEvent("BackupUI:EnableScheduledBackups", { 236 bubbles: true, 237 detail, 238 }) 239 ); 240 } 241 242 handleTogglePasswordOptions() { 243 this._showPasswordOptions = this.passwordOptionsCheckboxEl?.checked; 244 this._passwordsMatch = false; 245 } 246 247 updated(changedProperties) { 248 super.updated?.(changedProperties); 249 250 if (changedProperties.has("hideFilePathChooser")) { 251 // If hideFilePathChooser is true, show password options 252 this._showPasswordOptions = !!this.hideFilePathChooser; 253 254 // Uncheck the checkbox if it exists 255 if (this.passwordOptionsCheckboxEl) { 256 this.passwordOptionsCheckboxEl.checked = this._showPasswordOptions; 257 } 258 } 259 } 260 261 reset() { 262 this._showPasswordOptions = false; 263 this.passwordOptionsCheckboxEl.checked = false; 264 this._passwordsMatch = false; 265 this._inputPassValue = ""; 266 this.enableBackupErrorCode = 0; 267 this.disableSubmit = false; 268 // we don't want to reset the path when embedded in the spotlight 269 if (!this.embeddedFxBackupOptIn) { 270 this._newPath = ""; 271 this._newIconURL = ""; 272 this._newLabel = ""; 273 } 274 275 if (this.passwordOptionsExpandedEl) { 276 /** @type {import("./password-validation-inputs.mjs").default} */ 277 const passwordElement = this.passwordOptionsExpandedEl; 278 passwordElement.reset(); 279 } 280 281 if ( 282 this.embeddedFxBackupOptIn && 283 this.backupServiceState?.embeddedComponentPersistentData 284 ) { 285 this.dispatchEvent( 286 new CustomEvent("BackupUI:FlushEmbeddedComponentPersistentData", { 287 bubbles: true, 288 }) 289 ); 290 } 291 } 292 293 defaultFilePathInputTemplate() { 294 let filename = this.defaultLabel; 295 let iconURL = this.defaultIconURL || this.#placeholderIconURL; 296 297 const hasFilename = !!filename; 298 const l10nArgs = hasFilename 299 ? JSON.stringify({ recommendedFolder: filename }) 300 : null; 301 302 return html` 303 <input 304 id="backup-location-filepicker-input-default" 305 class="backup-location-filepicker-input" 306 type="text" 307 readonly 308 data-l10n-id=${hasFilename 309 ? "turn-on-scheduled-backups-location-default-folder" 310 : nothing} 311 data-l10n-args=${hasFilename ? l10nArgs : nothing} 312 data-l10n-attrs=${hasFilename ? "value" : nothing} 313 style=${`background-image: url(${iconURL})`} 314 /> 315 `; 316 } 317 318 /** 319 * Note: We also consider the embeddedComponentPersistentData since we might be in the 320 * Spotlight where we need this persistent data between screens. This state property should 321 * not be set if we are not in the Spotlight. 322 */ 323 customFilePathInputTemplate() { 324 let filename = 325 this._newLabel || 326 this.backupServiceState?.embeddedComponentPersistentData?.label; 327 let iconURL = 328 this._newIconURL || 329 this.backupServiceState?.embeddedComponentPersistentData?.iconURL || 330 this.#placeholderIconURL; 331 332 return html` 333 <input 334 id="backup-location-filepicker-input-custom" 335 class="backup-location-filepicker-input" 336 type="text" 337 readonly 338 .value=${filename} 339 style=${`background-image: url(${iconURL})`} 340 /> 341 `; 342 } 343 344 errorTemplate() { 345 return html` 346 <moz-message-bar 347 id="enable-backup-encryption-error" 348 type="error" 349 .messageL10nId=${getEnableErrorL10nId(this.enableBackupErrorCode)} 350 ></moz-message-bar> 351 `; 352 } 353 354 allOptionsTemplate() { 355 return html` 356 <fieldset id="all-controls"> 357 <div id="backup-location-controls"> 358 <label 359 id="backup-location-label" 360 for="backup-location-filepicker-input" 361 data-l10n-id=${this.filePathLabelL10nId || 362 "turn-on-scheduled-backups-location-label"} 363 ></label> 364 <div id="backup-location-filepicker"> 365 ${!this._newPath && 366 !this.backupServiceState?.embeddedComponentPersistentData?.path 367 ? this.defaultFilePathInputTemplate() 368 : this.customFilePathInputTemplate()} 369 <moz-button 370 id="backup-location-filepicker-button" 371 @click=${this.handleChooseLocation} 372 data-l10n-id="turn-on-scheduled-backups-location-choose-button" 373 aria-controls="backup-location-filepicker-input" 374 ></moz-button> 375 </div> 376 </div> 377 <fieldset id="sensitive-data-controls"> 378 <div id="sensitive-data-checkbox"> 379 <label 380 id="sensitive-data-checkbox-label" 381 for="sensitive-data-checkbox-input" 382 aria-controls="passwords" 383 aria-expanded=${this._showPasswordOptions} 384 > 385 <input 386 id="sensitive-data-checkbox-input" 387 .value=${this._showPasswordOptions} 388 @click=${this.handleTogglePasswordOptions} 389 type="checkbox" 390 /> 391 <span 392 id="sensitive-data-checkbox-span" 393 data-l10n-id="turn-on-scheduled-backups-encryption-label" 394 ></span> 395 </label> 396 <span 397 class="text-deemphasized" 398 data-l10n-id="settings-sensitive-data-encryption-description" 399 ></span> 400 </div> 401 402 ${this._showPasswordOptions ? this.passwordsTemplate() : null} 403 </fieldset> 404 </fieldset> 405 `; 406 } 407 408 passwordsTemplate() { 409 return html` 410 <password-validation-inputs 411 id="passwords" 412 .supportBaseLink=${this.supportBaseLink} 413 .createPasswordLabelL10nId=${this.createPasswordLabelL10nId} 414 ?embedded-fx-backup-opt-in=${this.embeddedFxBackupOptIn} 415 ></password-validation-inputs> 416 `; 417 } 418 419 contentTemplate() { 420 const hasEmbeddedPersistentData = 421 this.embeddedFxBackupOptIn && 422 this.backupServiceState?.embeddedComponentPersistentData?.path; 423 // All the situations where we want to disable submit: 424 // - passwords don't match 425 // - there's no destination folder 426 // - other unknown errors 427 if ( 428 (this._showPasswordOptions && !this._passwordsMatch) || 429 (!this._newPath && !this.defaultLabel && !hasEmbeddedPersistentData) || 430 this.enableBackupErrorCode != ERRORS.NONE 431 ) { 432 this.disableSubmit = true; 433 } else { 434 this.disableSubmit = false; 435 } 436 437 return html` 438 <form 439 id="backup-turn-on-scheduled-wrapper" 440 aria-labelledby="backup-turn-on-scheduled-header" 441 aria-describedby="backup-turn-on-scheduled-description" 442 part="form" 443 > 444 <h1 445 id="backup-turn-on-scheduled-header" 446 class="heading-medium" 447 data-l10n-id=${this.turnOnBackupHeaderL10nId || 448 "turn-on-scheduled-backups-header"} 449 ></h1> 450 <main id="backup-turn-on-scheduled-content"> 451 <div id="backup-turn-on-scheduled-description"> 452 <span 453 id="backup-turn-on-scheduled-description-span" 454 data-l10n-id="turn-on-scheduled-backups-description" 455 ></span> 456 <a 457 id="backup-turn-on-scheduled-learn-more-link" 458 is="moz-support-link" 459 support-page="firefox-backup" 460 data-l10n-id="turn-on-scheduled-backups-support-link" 461 utm-content="turn-on-backup" 462 ></a> 463 </div> 464 ${this.allOptionsTemplate()} 465 ${this.enableBackupErrorCode ? this.errorTemplate() : null} 466 </main> 467 468 <moz-button-group id="backup-turn-on-scheduled-button-group"> 469 <moz-button 470 id="backup-turn-on-scheduled-cancel-button" 471 @click=${this.close} 472 data-l10n-id=${this.turnOnBackupCancelBtnL10nId || 473 "turn-on-scheduled-backups-cancel-button"} 474 ></moz-button> 475 <moz-button 476 id="backup-turn-on-scheduled-confirm-button" 477 form="backup-turn-on-scheduled-wrapper" 478 @click=${this.handleConfirm} 479 type="primary" 480 data-l10n-id=${this.turnOnBackupConfirmBtnL10nId || 481 "turn-on-scheduled-backups-confirm-button"} 482 ?disabled=${this.disableSubmit} 483 ></moz-button> 484 </moz-button-group> 485 </form> 486 `; 487 } 488 489 render() { 490 return html` 491 <link 492 rel="stylesheet" 493 href="chrome://browser/content/backup/turn-on-scheduled-backups.css" 494 /> 495 ${this.contentTemplate()} 496 `; 497 } 498 } 499 500 customElements.define("turn-on-scheduled-backups", TurnOnScheduledBackups);