From 0ff62a5cc3a666427863be9627798a66a76a8508 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 24 Jul 2024 11:39:48 -0400 Subject: [PATCH] [PM-8338] New Autofill Settings Components (#10184) * add v2 autofill settings component * add and update entries in message catalog for new autofill settings view * add confirmation dialogs ahead of new tabs for browser settings from autofill settings * fix autofill on page load warning styling and improper concatenation * code cleanup --- apps/browser/src/_locales/en/messages.json | 97 +++- .../popup/settings/autofill-v1.component.html | 221 +++++++++ .../popup/settings/autofill-v1.component.ts | 301 ++++++++++++ .../popup/settings/autofill.component.html | 453 +++++++++--------- .../popup/settings/autofill.component.ts | 274 +++++++---- apps/browser/src/popup/app-routing.module.ts | 6 +- apps/browser/src/popup/app.module.ts | 4 +- libs/common/src/autofill/constants/index.ts | 24 + libs/common/src/autofill/types/index.ts | 13 +- 9 files changed, 1055 insertions(+), 338 deletions(-) create mode 100644 apps/browser/src/autofill/popup/settings/autofill-v1.component.html create mode 100644 apps/browser/src/autofill/popup/settings/autofill-v1.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 0ae0ca22a8..ad79d94cac 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -108,7 +108,7 @@ "message": "Copy security code" }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { "message": "Auto-fill login" @@ -789,12 +789,18 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, @@ -1227,11 +1233,20 @@ "message": "Show auto-fill menu on form fields", "description": "Represents the message for allowing the user to enable the auto-fill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1248,23 +1263,43 @@ "message": "When auto-fill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { "message": "Auto-fill on page load (if set up in Options)" @@ -1273,10 +1308,10 @@ "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -2004,7 +2039,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -2706,14 +2741,20 @@ "autofillSettings": { "message": "Auto-fill settings" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "message": "The autofill shortcut is not set. Change this in the browser's settings." }, "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "message": "The autofill shortcut is: $COMMAND$. Change this in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -3369,7 +3410,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3377,12 +3418,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Change your browser's autofill and password management settings.", + "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" + }, + "confirmContinueToHelpCenterKeyboardShortcutsContent": { + "message": "You can view and set extension shortcuts in your browser's settings.", + "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" + }, + "confirmContinueToBrowserPasswordManagementSettingsContent": { + "message": "Change your browser's autofill and password management settings.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "You can view and set extension shortcuts in your browser's settings.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html new file mode 100644 index 0000000000..9c7047c4cb --- /dev/null +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html @@ -0,0 +1,221 @@ +
+
+ +
+

+ {{ "autofill" | i18n }} +

+
+
+
+
+
+ +
+ +
+
+
+
+ + +
+ +
+
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+

{{ "additionalOptions" | i18n }}

+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts new file mode 100644 index 0000000000..261c6e459b --- /dev/null +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts @@ -0,0 +1,301 @@ +import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + InlineMenuVisibilitySetting, + ClearClipboardDelaySetting, +} from "@bitwarden/common/autofill/types"; +import { + UriMatchStrategy, + UriMatchStrategySetting, +} from "@bitwarden/common/models/domain/domain-service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { DialogService } from "@bitwarden/components"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { enableAccountSwitching } from "../../../platform/flags"; +import { AutofillService } from "../../services/abstractions/autofill.service"; + +@Component({ + selector: "app-autofill-v1", + templateUrl: "autofill-v1.component.html", +}) +export class AutofillV1Component implements OnInit { + protected canOverrideBrowserAutofillSetting = false; + protected defaultBrowserAutofillDisabled = false; + protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; + protected autoFillOverlayVisibilityOptions: any[]; + protected disablePasswordManagerLink: string; + enableAutoFillOnPageLoad = false; + autoFillOnPageLoadDefault = false; + autoFillOnPageLoadOptions: any[]; + enableContextMenuItem = false; + enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true? + clearClipboard: ClearClipboardDelaySetting; + clearClipboardOptions: any[]; + defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; + uriMatchOptions: any[]; + showCardsCurrentTab = false; + showIdentitiesCurrentTab = false; + autofillKeyboardHelperText: string; + accountSwitcherEnabled = false; + + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private domainSettingsService: DomainSettingsService, + private autofillService: AutofillService, + private dialogService: DialogService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, + private messagingService: MessagingService, + private vaultSettingsService: VaultSettingsService, + ) { + this.autoFillOverlayVisibilityOptions = [ + { + name: i18nService.t("autofillOverlayVisibilityOff"), + value: AutofillOverlayVisibility.Off, + }, + { + name: i18nService.t("autofillOverlayVisibilityOnFieldFocus"), + value: AutofillOverlayVisibility.OnFieldFocus, + }, + { + name: i18nService.t("autofillOverlayVisibilityOnButtonClick"), + value: AutofillOverlayVisibility.OnButtonClick, + }, + ]; + this.autoFillOnPageLoadOptions = [ + { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, + { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, + ]; + this.clearClipboardOptions = [ + { name: i18nService.t("never"), value: null }, + { name: i18nService.t("tenSeconds"), value: 10 }, + { name: i18nService.t("twentySeconds"), value: 20 }, + { name: i18nService.t("thirtySeconds"), value: 30 }, + { name: i18nService.t("oneMinute"), value: 60 }, + { name: i18nService.t("twoMinutes"), value: 120 }, + { name: i18nService.t("fiveMinutes"), value: 300 }, + ]; + this.uriMatchOptions = [ + { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, + { name: i18nService.t("host"), value: UriMatchStrategy.Host }, + { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, + { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, + { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, + { name: i18nService.t("never"), value: UriMatchStrategy.Never }, + ]; + + this.accountSwitcherEnabled = enableAccountSwitching(); + this.disablePasswordManagerLink = this.getDisablePasswordManagerLink(); + } + + async ngOnInit() { + this.canOverrideBrowserAutofillSetting = + this.platformUtilsService.isChrome() || + this.platformUtilsService.isEdge() || + this.platformUtilsService.isOpera() || + this.platformUtilsService.isVivaldi(); + + this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden(); + + this.autoFillOverlayVisibility = await firstValueFrom( + this.autofillSettingsService.inlineMenuVisibility$, + ); + + this.enableAutoFillOnPageLoad = await firstValueFrom( + this.autofillSettingsService.autofillOnPageLoad$, + ); + + this.autoFillOnPageLoadDefault = await firstValueFrom( + this.autofillSettingsService.autofillOnPageLoadDefault$, + ); + + this.enableContextMenuItem = await firstValueFrom( + this.autofillSettingsService.enableContextMenu$, + ); + + this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); + + this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); + + const defaultUriMatch = await firstValueFrom( + this.domainSettingsService.defaultUriMatchStrategy$, + ); + this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; + + const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); + await this.setAutofillKeyboardHelperText(command); + + this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$); + + this.showIdentitiesCurrentTab = await firstValueFrom( + this.vaultSettingsService.showIdentitiesCurrentTab$, + ); + } + + async updateAutoFillOverlayVisibility() { + await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); + await this.requestPrivacyPermission(); + } + + async updateAutoFillOnPageLoad() { + await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); + } + + async updateAutoFillOnPageLoadDefault() { + await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); + } + + async saveDefaultUriMatch() { + await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch); + } + + private async setAutofillKeyboardHelperText(command: string) { + if (command) { + this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command); + } else { + this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet"); + } + } + + async commandSettings() { + if (this.platformUtilsService.isChrome()) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.createNewTab("chrome://extensions/shortcuts"); + } else if (this.platformUtilsService.isOpera()) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.createNewTab("opera://extensions/shortcuts"); + } else if (this.platformUtilsService.isEdge()) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.createNewTab("edge://extensions/shortcuts"); + } else if (this.platformUtilsService.isVivaldi()) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.createNewTab("vivaldi://extensions/shortcuts"); + } else { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.createNewTab("https://bitwarden.com/help/keyboard-shortcuts"); + } + } + + private getDisablePasswordManagerLink(): string { + if (this.platformUtilsService.isChrome()) { + return "chrome://settings/autofill"; + } + if (this.platformUtilsService.isOpera()) { + return "opera://settings/autofill"; + } + if (this.platformUtilsService.isEdge()) { + return "edge://settings/passwords"; + } + if (this.platformUtilsService.isVivaldi()) { + return "vivaldi://settings/autofill"; + } + + return "https://bitwarden.com/help/disable-browser-autofill/"; + } + + protected openDisablePasswordManagerLink(event: Event) { + event.preventDefault(); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.createNewTab(this.disablePasswordManagerLink); + } + + async requestPrivacyPermission() { + if ( + this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off || + !this.canOverrideBrowserAutofillSetting || + (await this.browserAutofillSettingCurrentlyOverridden()) + ) { + return; + } + + await this.dialogService.openSimpleDialog({ + title: { key: "overrideDefaultBrowserAutofillTitle" }, + content: { key: "overrideDefaultBrowserAutofillDescription" }, + acceptButtonText: { key: "makeDefault" }, + acceptAction: async () => await this.handleOverrideDialogAccept(), + cancelButtonText: { key: "ignore" }, + type: "info", + }); + } + + async updateDefaultBrowserAutofillDisabled() { + const privacyPermissionGranted = await this.privacyPermissionGranted(); + if (!this.defaultBrowserAutofillDisabled && !privacyPermissionGranted) { + return; + } + + if ( + !privacyPermissionGranted && + !(await BrowserApi.requestPermission({ permissions: ["privacy"] })) + ) { + await this.dialogService.openSimpleDialog({ + title: { key: "privacyPermissionAdditionNotGrantedTitle" }, + content: { key: "privacyPermissionAdditionNotGrantedDescription" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "warning", + }); + this.defaultBrowserAutofillDisabled = false; + + return; + } + + BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled); + } + + private handleOverrideDialogAccept = async () => { + this.defaultBrowserAutofillDisabled = true; + await this.updateDefaultBrowserAutofillDisabled(); + }; + + async browserAutofillSettingCurrentlyOverridden() { + if (!this.canOverrideBrowserAutofillSetting) { + return false; + } + + if (!(await this.privacyPermissionGranted())) { + return false; + } + + return await BrowserApi.browserAutofillSettingsOverridden(); + } + + async privacyPermissionGranted(): Promise { + return await BrowserApi.permissionsGranted(["privacy"]); + } + + async updateContextMenuItem() { + await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); + this.messagingService.send("bgUpdateContextMenu"); + } + + async updateAutoTotpCopy() { + await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy); + } + + async saveClearClipboard() { + await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); + } + + async updateShowCardsCurrentTab() { + await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); + } + + async updateShowIdentitiesCurrentTab() { + await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); + } +} diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 30e00d4e64..5a7623f21c 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -1,221 +1,234 @@ -
-
- + + + + + + + +
+ + +

{{ "autofillSuggestionsSectionTitle" | i18n }}

+
+ + + + {{ "showInlineMenuLabel" | i18n }} + + {{ "showInlineMenuOnFormFieldsDescAlt" | i18n }} + + + + + + {{ "showInlineMenuOnIconSelectionLabel" | i18n }} + + + {{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }} + + {{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }} + + + + + + {{ + "overrideDefaultBrowserAutoFillSettings" | i18n + }} + + {{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }} + + {{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }} + + + + + + {{ "showCardsInVaultView" | i18n }} + + + + + {{ "showIdentitiesInVaultView" | i18n }} + + + +
+ + +

{{ "autofillKeyboardShortcutSectionTitle" | i18n }}

+
+ + + +
+ + +

{{ "enableAutoFillOnPageLoadSectionTitle" | i18n }}

+
+ + + {{ "enableAutoFillOnPageLoadDesc" | i18n }} + + + {{ "learnMoreAboutAutofillOnPageLoadLinkText" | i18n }} + + + + + {{ "enableAutoFillOnPageLoad" | i18n }} + + + {{ "defaultAutoFillOnPageLoad" | i18n }} + + + {{ "defaultAutoFillOnPageLoadDesc" | i18n }} + + + +
+ + +

{{ "additionalOptions" | i18n }}

+
+ + + + {{ "enableContextMenuItem" | i18n }} + + + + {{ "enableAutoTotpCopy" | i18n }} + + + {{ "clearClipboard" | i18n }} + + + {{ "clearClipboardDesc" | i18n }} + + + + {{ "defaultUriMatchDetection" | i18n }} + + + {{ "defaultUriMatchDetectionDesc" | i18n }} + + + +
-

- {{ "autofill" | i18n }} -

-
-
-
-
-
- -
- -
-
-
-
- - -
- -
-
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-

{{ "additionalOptions" | i18n }}

-
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
+ diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 7b8a1c32b4..44dcb04a13 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -1,12 +1,25 @@ +import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + AutofillOverlayVisibility, + BrowserClientVendors, + BrowserShortcutsUris, + ClearClipboardDelay, + DisablePasswordManagerUris, +} from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { - InlineMenuVisibilitySetting, + BrowserClientVendor, + BrowserShortcutsUri, ClearClipboardDelaySetting, + DisablePasswordManagerUri, + InlineMenuVisibilitySetting, } from "@bitwarden/common/autofill/types"; import { UriMatchStrategy, @@ -16,33 +29,77 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { DialogService } from "@bitwarden/components"; +import { + CardComponent, + CheckboxModule, + DialogService, + FormFieldModule, + IconButtonModule, + ItemModule, + LinkModule, + SectionComponent, + SectionHeaderComponent, + SelectModule, + TypographyModule, +} from "@bitwarden/components"; import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; -import { AutofillService } from "../../services/abstractions/autofill.service"; +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @Component({ - selector: "app-autofill", templateUrl: "autofill.component.html", + standalone: true, + imports: [ + CardComponent, + CheckboxModule, + CommonModule, + FormFieldModule, + FormsModule, + IconButtonModule, + ItemModule, + JslibModule, + LinkModule, + PopOutComponent, + PopupFooterComponent, + PopupHeaderComponent, + PopupPageComponent, + RouterModule, + SectionComponent, + SectionHeaderComponent, + SelectModule, + TypographyModule, + ], }) export class AutofillComponent implements OnInit { + /* + * Default values set here are used in component state operations + * until corresponding stored settings have loaded on init. + */ protected canOverrideBrowserAutofillSetting = false; protected defaultBrowserAutofillDisabled = false; - protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; - protected autoFillOverlayVisibilityOptions: any[]; - protected disablePasswordManagerLink: string; - enableAutoFillOnPageLoad = false; - autoFillOnPageLoadDefault = false; - autoFillOnPageLoadOptions: any[]; + protected inlineMenuVisibility: InlineMenuVisibilitySetting = + AutofillOverlayVisibility.OnFieldFocus; + protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown; + protected disablePasswordManagerURI: DisablePasswordManagerUri = + DisablePasswordManagerUris.Unknown; + protected browserShortcutsURI: BrowserShortcutsUri = BrowserShortcutsUris.Unknown; + protected browserClientIsUnknown: boolean; + enableAutofillOnPageLoad = false; + enableInlineMenu = false; + enableInlineMenuOnIconSelect = false; + autofillOnPageLoadDefault = false; + autofillOnPageLoadOptions: { name: string; value: boolean }[]; enableContextMenuItem = false; - enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true? + enableAutoTotpCopy = false; clearClipboard: ClearClipboardDelaySetting; - clearClipboardOptions: any[]; + clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[]; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; - uriMatchOptions: any[]; - showCardsCurrentTab = false; - showIdentitiesCurrentTab = false; + uriMatchOptions: { name: string; value: UriMatchStrategySetting }[]; + showCardsCurrentTab = true; + showIdentitiesCurrentTab = true; autofillKeyboardHelperText: string; accountSwitcherEnabled = false; @@ -50,38 +107,23 @@ export class AutofillComponent implements OnInit { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private domainSettingsService: DomainSettingsService, - private autofillService: AutofillService, private dialogService: DialogService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private messagingService: MessagingService, private vaultSettingsService: VaultSettingsService, ) { - this.autoFillOverlayVisibilityOptions = [ - { - name: i18nService.t("autofillOverlayVisibilityOff"), - value: AutofillOverlayVisibility.Off, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnFieldFocus"), - value: AutofillOverlayVisibility.OnFieldFocus, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnButtonClick"), - value: AutofillOverlayVisibility.OnButtonClick, - }, - ]; - this.autoFillOnPageLoadOptions = [ + this.autofillOnPageLoadOptions = [ { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, ]; this.clearClipboardOptions = [ - { name: i18nService.t("never"), value: null }, - { name: i18nService.t("tenSeconds"), value: 10 }, - { name: i18nService.t("twentySeconds"), value: 20 }, - { name: i18nService.t("thirtySeconds"), value: 30 }, - { name: i18nService.t("oneMinute"), value: 60 }, - { name: i18nService.t("twoMinutes"), value: 120 }, - { name: i18nService.t("fiveMinutes"), value: 300 }, + { name: i18nService.t("never"), value: ClearClipboardDelay.Never }, + { name: i18nService.t("tenSeconds"), value: ClearClipboardDelay.TenSeconds }, + { name: i18nService.t("twentySeconds"), value: ClearClipboardDelay.TwentySeconds }, + { name: i18nService.t("thirtySeconds"), value: ClearClipboardDelay.ThirtySeconds }, + { name: i18nService.t("oneMinute"), value: ClearClipboardDelay.OneMinute }, + { name: i18nService.t("twoMinutes"), value: ClearClipboardDelay.TwoMinutes }, + { name: i18nService.t("fiveMinutes"), value: ClearClipboardDelay.FiveMinutes }, ]; this.uriMatchOptions = [ { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, @@ -92,28 +134,32 @@ export class AutofillComponent implements OnInit { { name: i18nService.t("never"), value: UriMatchStrategy.Never }, ]; - this.accountSwitcherEnabled = enableAccountSwitching(); - this.disablePasswordManagerLink = this.getDisablePasswordManagerLink(); + this.browserClientVendor = this.getBrowserClientVendor(); + this.disablePasswordManagerURI = DisablePasswordManagerUris[this.browserClientVendor]; + this.browserShortcutsURI = BrowserShortcutsUris[this.browserClientVendor]; + this.browserClientIsUnknown = this.browserClientVendor === BrowserClientVendors.Unknown; } async ngOnInit() { - this.canOverrideBrowserAutofillSetting = - this.platformUtilsService.isChrome() || - this.platformUtilsService.isEdge() || - this.platformUtilsService.isOpera() || - this.platformUtilsService.isVivaldi(); - + this.canOverrideBrowserAutofillSetting = !this.browserClientIsUnknown; this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden(); - this.autoFillOverlayVisibility = await firstValueFrom( + this.inlineMenuVisibility = await firstValueFrom( this.autofillSettingsService.inlineMenuVisibility$, ); - this.enableAutoFillOnPageLoad = await firstValueFrom( + this.enableInlineMenuOnIconSelect = + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick; + + this.enableInlineMenu = + this.inlineMenuVisibility === AutofillOverlayVisibility.OnFieldFocus || + this.enableInlineMenuOnIconSelect; + + this.enableAutofillOnPageLoad = await firstValueFrom( this.autofillSettingsService.autofillOnPageLoad$, ); - this.autoFillOnPageLoadDefault = await firstValueFrom( + this.autofillOnPageLoadDefault = await firstValueFrom( this.autofillSettingsService.autofillOnPageLoadDefault$, ); @@ -140,17 +186,27 @@ export class AutofillComponent implements OnInit { ); } - async updateAutoFillOverlayVisibility() { - await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); + async updateInlineMenuVisibility() { + if (!this.enableInlineMenu) { + this.enableInlineMenuOnIconSelect = false; + } + + const newInlineMenuVisibilityValue = this.enableInlineMenuOnIconSelect + ? AutofillOverlayVisibility.OnButtonClick + : this.enableInlineMenu + ? AutofillOverlayVisibility.OnFieldFocus + : AutofillOverlayVisibility.Off; + + await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue); await this.requestPrivacyPermission(); } - async updateAutoFillOnPageLoad() { - await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); + async updateAutofillOnPageLoad() { + await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutofillOnPageLoad); } - async updateAutoFillOnPageLoadDefault() { - await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); + async updateAutofillOnPageLoadDefault() { + await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autofillOnPageLoadDefault); } async saveDefaultUriMatch() { @@ -165,57 +221,81 @@ export class AutofillComponent implements OnInit { } } - async commandSettings() { + private getBrowserClientVendor(): BrowserClientVendor { if (this.platformUtilsService.isChrome()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("chrome://extensions/shortcuts"); - } else if (this.platformUtilsService.isOpera()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("opera://extensions/shortcuts"); - } else if (this.platformUtilsService.isEdge()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("edge://extensions/shortcuts"); - } else if (this.platformUtilsService.isVivaldi()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("vivaldi://extensions/shortcuts"); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("https://bitwarden.com/help/keyboard-shortcuts"); + return BrowserClientVendors.Chrome; } - } - private getDisablePasswordManagerLink(): string { - if (this.platformUtilsService.isChrome()) { - return "chrome://settings/autofill"; - } if (this.platformUtilsService.isOpera()) { - return "opera://settings/autofill"; - } - if (this.platformUtilsService.isEdge()) { - return "edge://settings/passwords"; - } - if (this.platformUtilsService.isVivaldi()) { - return "vivaldi://settings/autofill"; + return BrowserClientVendors.Opera; } - return "https://bitwarden.com/help/disable-browser-autofill/"; + if (this.platformUtilsService.isEdge()) { + return BrowserClientVendors.Edge; + } + + if (this.platformUtilsService.isVivaldi()) { + return BrowserClientVendors.Vivaldi; + } + + return BrowserClientVendors.Unknown; } - protected openDisablePasswordManagerLink(event: Event) { + protected async openURI(event: Event, uri: BrowserShortcutsUri | DisablePasswordManagerUri) { event.preventDefault(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(this.disablePasswordManagerLink); + + // If the destination is a password management settings page, ask the user to confirm before proceeding + if (uri === DisablePasswordManagerUris[this.browserClientVendor]) { + await this.dialogService.openSimpleDialog({ + ...(this.browserClientIsUnknown + ? { + content: { key: "confirmContinueToHelpCenterPasswordManagementContent" }, + title: { key: "confirmContinueToHelpCenter" }, + } + : { + content: { key: "confirmContinueToBrowserPasswordManagementSettingsContent" }, + title: { key: "confirmContinueToBrowserSettingsTitle" }, + }), + acceptButtonText: { key: "continue" }, + acceptAction: async () => { + await BrowserApi.createNewTab(uri); + }, + cancelButtonText: { key: "cancel" }, + type: "info", + }); + + return; + } + + // If the destination is a browser shortcut settings page, ask the user to confirm before proceeding + if (uri === BrowserShortcutsUris[this.browserClientVendor]) { + await this.dialogService.openSimpleDialog({ + ...(this.browserClientIsUnknown + ? { + content: { key: "confirmContinueToHelpCenterKeyboardShortcutsContent" }, + title: { key: "confirmContinueToHelpCenter" }, + } + : { + content: { key: "confirmContinueToBrowserKeyboardShortcutSettingsContent" }, + title: { key: "confirmContinueToBrowserSettingsTitle" }, + }), + acceptButtonText: { key: "continue" }, + acceptAction: async () => { + await BrowserApi.createNewTab(uri); + }, + cancelButtonText: { key: "cancel" }, + type: "info", + }); + + return; + } + + await BrowserApi.createNewTab(uri); } async requestPrivacyPermission() { if ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off || + this.inlineMenuVisibility === AutofillOverlayVisibility.Off || !this.canOverrideBrowserAutofillSetting || (await this.browserAutofillSettingCurrentlyOverridden()) ) { @@ -225,9 +305,9 @@ export class AutofillComponent implements OnInit { await this.dialogService.openSimpleDialog({ title: { key: "overrideDefaultBrowserAutofillTitle" }, content: { key: "overrideDefaultBrowserAutofillDescription" }, - acceptButtonText: { key: "makeDefault" }, + acceptButtonText: { key: "continue" }, acceptAction: async () => await this.handleOverrideDialogAccept(), - cancelButtonText: { key: "ignore" }, + cancelButtonText: { key: "cancel" }, type: "info", }); } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 54543a4212..bb9d038378 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -38,6 +38,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; @@ -278,12 +279,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "export" }, }), - { + ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { path: "autofill", - component: AutofillComponent, canActivate: [AuthGuard], data: { state: "autofill" }, - }, + }), { path: "account-security", component: AccountSecurityComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d43e680e9b..d3caef579c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -36,6 +36,7 @@ import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; @@ -93,6 +94,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, + AutofillComponent, ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, @@ -180,7 +182,7 @@ import "../platform/popup/locales"; RemovePasswordComponent, VaultSelectComponent, Fido2Component, - AutofillComponent, + AutofillV1Component, EnvironmentSelectorComponent, AccountSwitcherComponent, ], diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index efbd089642..d3b5e6ee10 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -61,3 +61,27 @@ export const AutofillOverlayVisibility = { OnButtonClick: 1, OnFieldFocus: 2, } as const; + +export const BrowserClientVendors = { + Chrome: "Chrome", + Opera: "Opera", + Edge: "Edge", + Vivaldi: "Vivaldi", + Unknown: "Unknown", +} as const; + +export const BrowserShortcutsUris = { + Chrome: "chrome://extensions/shortcuts", + Opera: "opera://extensions/shortcuts", + Edge: "edge://extensions/shortcuts", + Vivaldi: "vivaldi://extensions/shortcuts", + Unknown: "https://bitwarden.com/help/keyboard-shortcuts", +} as const; + +export const DisablePasswordManagerUris = { + Chrome: "chrome://settings/autofill", + Opera: "opera://settings/autofill", + Edge: "edge://settings/passwords", + Vivaldi: "vivaldi://settings/autofill", + Unknown: "https://bitwarden.com/help/disable-browser-autofill/", +} as const; diff --git a/libs/common/src/autofill/types/index.ts b/libs/common/src/autofill/types/index.ts index be5d98f4e0..9a5a434d9e 100644 --- a/libs/common/src/autofill/types/index.ts +++ b/libs/common/src/autofill/types/index.ts @@ -1,7 +1,18 @@ -import { ClearClipboardDelay, AutofillOverlayVisibility } from "../constants"; +import { + AutofillOverlayVisibility, + BrowserClientVendors, + BrowserShortcutsUris, + ClearClipboardDelay, + DisablePasswordManagerUris, +} from "../constants"; export type ClearClipboardDelaySetting = (typeof ClearClipboardDelay)[keyof typeof ClearClipboardDelay]; export type InlineMenuVisibilitySetting = (typeof AutofillOverlayVisibility)[keyof typeof AutofillOverlayVisibility]; + +export type BrowserClientVendor = (typeof BrowserClientVendors)[keyof typeof BrowserClientVendors]; +export type BrowserShortcutsUri = (typeof BrowserShortcutsUris)[keyof typeof BrowserShortcutsUris]; +export type DisablePasswordManagerUri = + (typeof DisablePasswordManagerUris)[keyof typeof DisablePasswordManagerUris];