diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e05d3fcd16..8d784a9678 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -736,6 +736,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." + }, "addedItem": { "message": "Item added" }, @@ -776,6 +780,9 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, @@ -1967,6 +1974,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1976,6 +1987,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { @@ -1985,6 +2005,9 @@ } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html new file mode 100644 index 0000000000..8f78ac1640 --- /dev/null +++ b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html @@ -0,0 +1,91 @@ +
diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts new file mode 100644 index 0000000000..362ac4896c --- /dev/null +++ b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts @@ -0,0 +1,141 @@ +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { enableAccountSwitching } from "../../../platform/flags"; + +interface ExcludedDomain { + uri: string; + showCurrentUris: boolean; +} + +const BroadcasterSubscriptionId = "excludedDomains"; + +@Component({ + selector: "app-excluded-domains-v1", + templateUrl: "excluded-domains-v1.component.html", +}) +export class ExcludedDomainsV1Component implements OnInit, OnDestroy { + excludedDomains: ExcludedDomain[] = []; + existingExcludedDomains: ExcludedDomain[] = []; + currentUris: string[]; + loadCurrentUrisTimeout: number; + accountSwitcherEnabled = false; + + constructor( + private domainSettingsService: DomainSettingsService, + private i18nService: I18nService, + private router: Router, + private broadcasterService: BroadcasterService, + private ngZone: NgZone, + private platformUtilsService: PlatformUtilsService, + ) { + this.accountSwitcherEnabled = enableAccountSwitching(); + } + + async ngOnInit() { + const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + if (savedDomains) { + for (const uri of Object.keys(savedDomains)) { + this.excludedDomains.push({ uri: uri, showCurrentUris: false }); + this.existingExcludedDomains.push({ uri: uri, showCurrentUris: false }); + } + } + + await this.loadCurrentUris(); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + // 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 + this.ngZone.run(async () => { + switch (message.command) { + case "tabChanged": + case "windowChanged": + if (this.loadCurrentUrisTimeout != null) { + window.clearTimeout(this.loadCurrentUrisTimeout); + } + this.loadCurrentUrisTimeout = window.setTimeout( + async () => await this.loadCurrentUris(), + 500, + ); + break; + default: + break; + } + }); + }); + } + + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + async addUri() { + this.excludedDomains.push({ uri: "", showCurrentUris: false }); + } + + async removeUri(i: number) { + this.excludedDomains.splice(i, 1); + } + + async submit() { + const savedDomains: { [name: string]: null } = {}; + const newExcludedDomains = this.getNewlyAddedDomains(this.excludedDomains); + for (const domain of this.excludedDomains) { + const resp = newExcludedDomains.filter((e) => e.uri === domain.uri); + if (resp.length === 0) { + savedDomains[domain.uri] = null; + } else { + if (domain.uri && domain.uri !== "") { + const validDomain = Utils.getHostname(domain.uri); + if (!validDomain) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("excludedDomainsInvalidDomain", domain.uri), + ); + return; + } + savedDomains[validDomain] = null; + } + } + } + + await this.domainSettingsService.setNeverDomains(savedDomains); + // 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 + this.router.navigate(["/tabs/settings"]); + } + + trackByFunction(index: number, item: any) { + return index; + } + + getNewlyAddedDomains(domain: ExcludedDomain[]): ExcludedDomain[] { + const result = this.excludedDomains.filter( + (newDomain) => + !this.existingExcludedDomains.some((oldDomain) => newDomain.uri === oldDomain.uri), + ); + return result; + } + + toggleUriInput(domain: ExcludedDomain) { + domain.showCurrentUris = !domain.showCurrentUris; + } + + async loadCurrentUris() { + const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); + if (tabs) { + const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url))); + uriSet.delete(null); + this.currentUris = Array.from(uriSet); + } + } +} diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains.component.html index 8f78ac1640..8f909a336b 100644 --- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.html +++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.html @@ -1,91 +1,63 @@ -