| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| import 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js'; |
| import 'chrome://resources/cr_elements/cr_shared_style.css.js'; |
| import 'chrome://resources/cr_elements/cr_spinner_style.css.js'; |
| import './shared_style.css.js'; |
| import './prefs/pref_toggle_button.js'; |
| import './user_utils_mixin.js'; |
| import '/shared/settings/controls/extension_controlled_indicator.js'; |
| import './dialogs/disconnect_cloud_authenticator_dialog.js'; |
| |
| import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js'; |
| import {HelpBubbleMixin} from 'chrome://resources/cr_components/help_bubble/help_bubble_mixin.js'; |
| import type {CrLinkRowElement} from 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js'; |
| import type {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js'; |
| import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js'; |
| import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js'; |
| import {assert, assertNotReached} from 'chrome://resources/js/assert.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {OpenWindowProxyImpl} from 'chrome://resources/js/open_window_proxy.js'; |
| import {PluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js'; |
| import type {DomRepeatEvent} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| // <if expr="is_win or is_macosx"> |
| import {PasskeysBrowserProxyImpl} from './passkeys_browser_proxy.js'; |
| // </if> |
| import type {BlockedSite, BlockedSitesListChangedListener, CredentialsChangedListener, ShouldShowAccountStorageToggleChangedListener} from './password_manager_proxy.js'; |
| import {PasswordManagerImpl} from './password_manager_proxy.js'; |
| import type {PrefToggleButtonElement} from './prefs/pref_toggle_button.js'; |
| import type {Route} from './router.js'; |
| import {Page, RouteObserverMixin, Router, UrlParam} from './router.js'; |
| import {getTemplate} from './settings_section.html.js'; |
| import {BatchUploadPasswordsEntryPoint, SyncBrowserProxyImpl, TrustedVaultBannerState} from './sync_browser_proxy.js'; |
| import {UserUtilMixin} from './user_utils_mixin.js'; |
| |
| export interface SettingsSectionElement { |
| $: { |
| accountStorageToggle: PrefToggleButtonElement, |
| autosigninToggle: PrefToggleButtonElement, |
| blockedSitesList: HTMLElement, |
| passkeyUpgradeToggle: PrefToggleButtonElement, |
| passwordToggle: PrefToggleButtonElement, |
| toast: CrToastElement, |
| trustedVaultBanner: CrLinkRowElement, |
| }; |
| } |
| |
| const PASSWORD_MANAGER_ADD_SHORTCUT_ELEMENT_ID = |
| 'PasswordManagerUI::kAddShortcutElementId'; |
| const PASSWORD_MANAGER_ADD_SHORTCUT_CUSTOM_EVENT_ID = |
| 'PasswordManagerUI::kAddShortcutCustomEventId'; |
| export const PASSWORD_MANAGER_ACCOUNT_STORE_TOGGLE_ELEMENT_ID = |
| 'PasswordManagerUI::kAccountStoreToggleElementId'; |
| |
| const SettingsSectionElementBase = HelpBubbleMixin(RouteObserverMixin( |
| PrefsMixin(UserUtilMixin(WebUiListenerMixin(I18nMixin(PolymerElement)))))); |
| |
| export class SettingsSectionElement extends SettingsSectionElementBase { |
| static get is() { |
| return 'settings-section'; |
| } |
| |
| static get template() { |
| return getTemplate(); |
| } |
| |
| static get properties() { |
| return { |
| /** An array of blocked sites to display. */ |
| blockedSites_: { |
| type: Array, |
| value: () => [], |
| }, |
| |
| // <if expr="is_win or is_macosx or is_chromeos"> |
| isBiometricAuthenticationForFillingToggleVisible_: { |
| type: Boolean, |
| value() { |
| return loadTimeData.getBoolean( |
| 'biometricAuthenticationForFillingToggleVisible'); |
| }, |
| }, |
| // </if> |
| |
| isPasskeyUpgradeSettingsToggleVisible_: { |
| type: Boolean, |
| value() { |
| return loadTimeData.getBoolean('passkeyUpgradeSettingsToggleVisible'); |
| }, |
| }, |
| |
| isAutomatedPasswordChangeVisible_: { |
| type: Boolean, |
| value() { |
| return loadTimeData.getBoolean('passwordChangeAvailable'); |
| }, |
| }, |
| |
| hasPasswordsToExport_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| hasPasskeys_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| passwordManagerDisabled_: { |
| type: Boolean, |
| computed: 'computePasswordManagerDisabled_(' + |
| 'prefs.credentials_enable_service.enforcement, ' + |
| 'prefs.credentials_enable_service.value)', |
| }, |
| |
| /** The visibility state of the trusted vault banner. */ |
| trustedVaultBannerState_: { |
| type: Object, |
| value: TrustedVaultBannerState.NOT_SHOWN, |
| }, |
| |
| movePasswordsLabel_: { |
| type: String, |
| value: '', |
| }, |
| |
| canAddShortcut_: { |
| type: Boolean, |
| value() { |
| return loadTimeData.getBoolean('canAddShortcut'); |
| }, |
| }, |
| |
| isPasswordManagerPinAvailable_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| isConnectedToCloudAuthenticator_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| isDisconnectCloudAuthenticatorInProgress_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| toastMessage_: { |
| type: String, |
| value: '', |
| }, |
| |
| showDisconnectCloudAuthenticatorDialog_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| localPasswordCount_: { |
| type: Number, |
| value: 0, |
| }, |
| |
| shouldShowAccountStorageSettingToggle_: { |
| type: Boolean, |
| value: false, |
| }, |
| }; |
| } |
| |
| static get observers() { |
| return [ |
| 'updateIsPasswordManagerPinAvailable_(' + |
| 'isSyncingPasswords, isAccountStoreUser)', |
| 'updateIsCloudAuthenticatorConnected_(' + |
| 'isSyncingPasswords, isAccountStoreUser)', |
| ]; |
| } |
| |
| declare private blockedSites_: BlockedSite[]; |
| // <if expr="is_win or is_macosx or is_chromeos"> |
| declare private isBiometricAuthenticationForFillingToggleVisible_: boolean; |
| // </if> |
| declare private hasPasskeys_: boolean; |
| declare private passwordManagerDisabled_: boolean; |
| declare private hasPasswordsToExport_: boolean; |
| declare private isPasskeyUpgradeSettingsToggleVisible_: boolean; |
| declare private isAutomatedPasswordChangeVisible_: boolean; |
| declare private canAddShortcut_: boolean; |
| declare private trustedVaultBannerState_: TrustedVaultBannerState; |
| declare private movePasswordsLabel_: string; |
| declare private isPasswordManagerPinAvailable_: boolean; |
| declare private isConnectedToCloudAuthenticator_: boolean; |
| declare private isDisconnectCloudAuthenticatorInProgress_: boolean; |
| declare private toastMessage_: string; |
| declare private showDisconnectCloudAuthenticatorDialog_: boolean; |
| // This variable depend on the sync service API, which the Batch Upload Dialog |
| // uses. |
| declare private localPasswordCount_: number; |
| declare private shouldShowAccountStorageSettingToggle_: boolean; |
| |
| private setBlockedSitesListListener_: BlockedSitesListChangedListener|null = |
| null; |
| private setCredentialsChangedListener_: CredentialsChangedListener|null = |
| null; |
| private shouldShowAccountStorageSettingToggleListener_: |
| ShouldShowAccountStorageToggleChangedListener|null = null; |
| |
| override ready() { |
| super.ready(); |
| |
| chrome.metricsPrivate.recordBoolean( |
| 'PasswordManager.OpenedAsShortcut', |
| window.matchMedia('(display-mode: standalone)').matches); |
| } |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| const updateLocalPasswordCount = (localPasswordCount: number) => { |
| this.updateLocalPasswordCount_(localPasswordCount); |
| }; |
| const syncBrowserProxy = SyncBrowserProxyImpl.getInstance(); |
| syncBrowserProxy.getLocalPasswordCount().then(updateLocalPasswordCount); |
| |
| this.setBlockedSitesListListener_ = blockedSites => { |
| this.blockedSites_ = blockedSites; |
| }; |
| PasswordManagerImpl.getInstance().getBlockedSitesList().then( |
| blockedSites => this.blockedSites_ = blockedSites); |
| PasswordManagerImpl.getInstance().addBlockedSitesListChangedListener( |
| this.setBlockedSitesListListener_); |
| |
| this.addWebUiListener( |
| 'sync-service-local-password-count', updateLocalPasswordCount); |
| |
| this.setCredentialsChangedListener_ = |
| (passwords: chrome.passwordsPrivate.PasswordUiEntry[]) => { |
| this.hasPasswordsToExport_ = passwords.length > 0; |
| // Update the local password count based on the SyncService API |
| // whenever the password list was modified. |
| syncBrowserProxy.getLocalPasswordCount().then( |
| (localPasswordCount: number) => { |
| this.updateLocalPasswordCount_(localPasswordCount); |
| }); |
| }; |
| PasswordManagerImpl.getInstance().getSavedPasswordList().then( |
| this.setCredentialsChangedListener_); |
| PasswordManagerImpl.getInstance().addSavedPasswordListChangedListener( |
| this.setCredentialsChangedListener_); |
| |
| this.shouldShowAccountStorageSettingToggleListener_ = show => { |
| this.shouldShowAccountStorageSettingToggle_ = show; |
| }; |
| PasswordManagerImpl.getInstance() |
| .shouldShowAccountStorageSettingToggle() |
| .then(this.shouldShowAccountStorageSettingToggleListener_); |
| PasswordManagerImpl.getInstance() |
| .addShouldShowAccountStorageSettingToggleListener( |
| this.shouldShowAccountStorageSettingToggleListener_); |
| |
| const trustedVaultStateChanged = (state: TrustedVaultBannerState) => { |
| this.trustedVaultBannerState_ = state; |
| }; |
| syncBrowserProxy.getTrustedVaultBannerState().then( |
| trustedVaultStateChanged); |
| this.addWebUiListener( |
| 'trusted-vault-banner-state-changed', trustedVaultStateChanged); |
| // TODO(crbug.com/331611435): add listener for enclave availability and |
| // trigger `updateIsPasswordManagerPinAvailable_`. |
| this.updateIsPasswordManagerPinAvailable_(); |
| // Checks if the Chrome client is connected to / registered with the |
| // Cloud Authenticator. If the client is connected, then a button to |
| // disconnect the client is displayed. |
| this.updateIsCloudAuthenticatorConnected_(); |
| |
| // <if expr="is_win or is_macosx"> |
| PasskeysBrowserProxyImpl.getInstance().hasPasskeys().then(hasPasskeys => { |
| this.hasPasskeys_ = hasPasskeys; |
| }); |
| // </if> |
| |
| const accountStorageToggleRoot = this.$.accountStorageToggle.shadowRoot; |
| this.registerHelpBubble( |
| PASSWORD_MANAGER_ACCOUNT_STORE_TOGGLE_ELEMENT_ID, |
| accountStorageToggleRoot!.querySelector('#control')!); |
| } |
| |
| override disconnectedCallback() { |
| super.disconnectedCallback(); |
| assert(this.setBlockedSitesListListener_); |
| PasswordManagerImpl.getInstance().removeBlockedSitesListChangedListener( |
| this.setBlockedSitesListListener_); |
| this.setBlockedSitesListListener_ = null; |
| |
| assert(this.setCredentialsChangedListener_); |
| PasswordManagerImpl.getInstance().removeSavedPasswordListChangedListener( |
| this.setCredentialsChangedListener_); |
| this.setCredentialsChangedListener_ = null; |
| |
| assert(this.shouldShowAccountStorageSettingToggleListener_); |
| PasswordManagerImpl.getInstance() |
| .removeShouldShowAccountStorageSettingToggleListener( |
| this.shouldShowAccountStorageSettingToggleListener_); |
| this.shouldShowAccountStorageSettingToggleListener_ = null; |
| |
| this.$.toast.hide(); |
| } |
| |
| override currentRouteChanged(newRoute: Route, oldRoute?: Route): void { |
| if (newRoute.page === Page.SETTINGS && |
| oldRoute?.page === Page.PASSWORD_CHANGE && |
| this.isAutomatedPasswordChangeVisible_) { |
| setTimeout(() => { |
| const automatedPasswordChangeRow = |
| this.shadowRoot!.querySelector<HTMLElement>( |
| '#automatedPasswordChange'); |
| if (automatedPasswordChangeRow) { |
| automatedPasswordChangeRow.focus(); |
| } |
| }, 0); |
| } |
| |
| const triggerImportParam = |
| newRoute.queryParameters.get(UrlParam.START_IMPORT) || ''; |
| if (triggerImportParam === 'true') { |
| const importer = this.shadowRoot!.querySelector('passwords-importer'); |
| assert(importer); |
| importer.launchImport(); |
| const params = new URLSearchParams(); |
| Router.getInstance().updateRouterParams(params); |
| } |
| } |
| |
| private onShortcutBannerDomChanged_() { |
| const addShortcutBanner = this.root!.querySelector('#addShortcutBanner'); |
| if (addShortcutBanner) { |
| this.registerHelpBubble( |
| PASSWORD_MANAGER_ADD_SHORTCUT_ELEMENT_ID, addShortcutBanner); |
| } |
| } |
| |
| private onAddShortcutClick_() { |
| this.notifyHelpBubbleAnchorCustomEvent( |
| PASSWORD_MANAGER_ADD_SHORTCUT_ELEMENT_ID, |
| PASSWORD_MANAGER_ADD_SHORTCUT_CUSTOM_EVENT_ID, |
| ); |
| // TODO(crbug.com/40236982): Record metrics on all entry points usage. |
| // TODO(crbug.com/40236982): Hide the button for users after the shortcut is |
| // installed. |
| PasswordManagerImpl.getInstance().showAddShortcutDialog(); |
| } |
| |
| /** |
| * Fires an event that should delete the blocked password entry. |
| */ |
| private onRemoveBlockedSiteClick_( |
| event: DomRepeatEvent<chrome.passwordsPrivate.ExceptionEntry>) { |
| PasswordManagerImpl.getInstance().removeBlockedSite(event.model.item.id); |
| } |
| |
| // <if expr="is_win or is_macosx or is_chromeos"> |
| private switchBiometricAuthBeforeFillingState_(e: Event) { |
| const biometricAuthenticationForFillingToggle = |
| e.target as PrefToggleButtonElement; |
| assert(biometricAuthenticationForFillingToggle); |
| PasswordManagerImpl.getInstance().switchBiometricAuthBeforeFillingState(); |
| } |
| // </if> |
| |
| private onTrustedVaultBannerClick_() { |
| switch (this.trustedVaultBannerState_) { |
| case TrustedVaultBannerState.OPTED_IN: |
| OpenWindowProxyImpl.getInstance().openUrl( |
| loadTimeData.getString('trustedVaultLearnMoreUrl')); |
| break; |
| case TrustedVaultBannerState.OFFER_OPT_IN: |
| OpenWindowProxyImpl.getInstance().openUrl( |
| loadTimeData.getString('trustedVaultOptInUrl')); |
| break; |
| case TrustedVaultBannerState.NOT_SHOWN: |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| private getTrustedVaultBannerTitle_(): string { |
| switch (this.trustedVaultBannerState_) { |
| case TrustedVaultBannerState.OPTED_IN: |
| return this.i18n('trustedVaultBannerLabelOptedIn'); |
| case TrustedVaultBannerState.OFFER_OPT_IN: |
| return this.i18n('trustedVaultBannerLabelOfferOptIn'); |
| case TrustedVaultBannerState.NOT_SHOWN: |
| return ''; |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| private getTrustedVaultBannerDescription_(): string { |
| switch (this.trustedVaultBannerState_) { |
| case TrustedVaultBannerState.OPTED_IN: |
| return this.i18n('trustedVaultBannerSubLabelOptedIn'); |
| case TrustedVaultBannerState.OFFER_OPT_IN: |
| return this.i18n('trustedVaultBannerSubLabelOfferOptIn'); |
| case TrustedVaultBannerState.NOT_SHOWN: |
| return ''; |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| private shouldHideTrustedVaultBanner_(): boolean { |
| return this.trustedVaultBannerState_ === TrustedVaultBannerState.NOT_SHOWN; |
| } |
| |
| private getAriaLabelForBlockedSite_( |
| blockedSite: chrome.passwordsPrivate.ExceptionEntry): string { |
| return this.i18n('removeBlockedAriaDescription', blockedSite.urls.shown); |
| } |
| |
| private changeAccountStorageEnabled_() { |
| if (this.isAccountStoreUser) { |
| this.disableAccountStorage(); |
| } else { |
| this.enableAccountStorage(); |
| } |
| } |
| |
| private getAccountStorageSubLabel_(accountEmail: string): string { |
| return this.i18n('accountStorageToggleSubLabel', accountEmail); |
| } |
| |
| // <if expr="is_win or is_macosx"> |
| private onManagePasskeysClick_() { |
| PasskeysBrowserProxyImpl.getInstance().managePasskeys(); |
| } |
| // </if> |
| |
| private computePasswordManagerDisabled_(): boolean { |
| const pref = this.getPref('credentials_enable_service'); |
| |
| const isPolicyEnforced = |
| pref.enforcement === chrome.settingsPrivate.Enforcement.ENFORCED; |
| |
| const isPolicyControlledByExtension = |
| pref.controlledBy === chrome.settingsPrivate.ControlledBy.EXTENSION; |
| |
| if (isPolicyControlledByExtension) { |
| return false; |
| } |
| |
| return !pref.value && isPolicyEnforced; |
| } |
| |
| private onMovePasswordsClicked_(e: Event) { |
| e.preventDefault(); |
| SyncBrowserProxyImpl.getInstance().openBatchUpload( |
| BatchUploadPasswordsEntryPoint.PASSWORD_MANAGER); |
| } |
| |
| private shouldShowMovePasswordsEntry_(): boolean { |
| // Only show the move password entry if there are passwords returned from |
| // the sync service API. This is needed to be consistent with the |
| // availability of data in the dialog which uses the same API. |
| return this.localPasswordCount_ > 0; |
| } |
| |
| private getAriaLabelMovePasswordsButton_(): string { |
| return [ |
| this.movePasswordsLabel_, |
| this.i18n('movePasswordsInSettingsSubLabel'), |
| this.i18n('moveSinglePasswordButton'), |
| ].join('. '); |
| } |
| |
| // This updates the local password count coming from the Sync Service API. |
| private async updateLocalPasswordCount_(localPasswordCount: number) { |
| this.localPasswordCount_ = localPasswordCount; |
| |
| this.movePasswordsLabel_ = |
| await PluralStringProxyImpl.getInstance().getPluralString( |
| 'deviceOnlyPasswordsIconTooltip', this.localPasswordCount_); |
| } |
| |
| private updateIsPasswordManagerPinAvailable_() { |
| PasswordManagerImpl.getInstance().isPasswordManagerPinAvailable().then( |
| available => this.isPasswordManagerPinAvailable_ = |
| available && (this.isSyncingPasswords || this.isAccountStoreUser)); |
| } |
| |
| private onChangePasswordManagerPinRowClick_() { |
| PasswordManagerImpl.getInstance().changePasswordManagerPin().then( |
| this.showToastForPasswordChange_.bind(this)); |
| } |
| |
| private updateIsCloudAuthenticatorConnected_() { |
| PasswordManagerImpl.getInstance().isConnectedToCloudAuthenticator().then( |
| connected => this.isConnectedToCloudAuthenticator_ = |
| connected && (this.isSyncingPasswords || this.isAccountStoreUser)); |
| } |
| |
| private onDisconnectCloudAuthenticatorClick_() { |
| this.showDisconnectCloudAuthenticatorDialog_ = true; |
| } |
| |
| private onCloseDisconnectCloudAuthenticatorDialog_(): void { |
| this.showDisconnectCloudAuthenticatorDialog_ = false; |
| } |
| |
| private onDisconnectCloudAuthenticator_(e: CustomEvent): void { |
| this.isDisconnectCloudAuthenticatorInProgress_ = false; |
| this.updateIsCloudAuthenticatorConnected_(); |
| this.updateIsPasswordManagerPinAvailable_(); |
| if (e.detail.success) { |
| this.showToastForCloudAuthenticatorDisconnected_(); |
| } |
| } |
| |
| private showToastForCloudAuthenticatorDisconnected_(): void { |
| this.toastMessage_ = this.i18n('disconnectCloudAuthenticatorToastMessage'); |
| this.$.toast.show(); |
| } |
| |
| private getAriaLabelForCloudAuthenticatorButton_(): string { |
| return [ |
| this.i18n('disconnectCloudAuthenticatorTitle'), |
| this.i18n('disconnectCloudAuthenticatorDescription'), |
| ].join('. '); |
| } |
| |
| private showToastForPasswordChange_(success: boolean): void { |
| if (!success) { |
| return; |
| } |
| this.toastMessage_ = this.i18n('passwordManagerPinChanged'); |
| this.$.toast.show(); |
| } |
| |
| private onAutomatedPasswordChangeClick_() { |
| Router.getInstance().navigateTo(Page.PASSWORD_CHANGE); |
| } |
| |
| private getAriaLabelForAutomatedPasswordChange_(): string { |
| return [ |
| this.i18n('automatedPasswordChangeTitle'), |
| this.i18n('automatedPasswordChangeDescription'), |
| ].join('. '); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'settings-section': SettingsSectionElement; |
| } |
| } |
| |
| customElements.define(SettingsSectionElement.is, SettingsSectionElement); |