From 3b0f27f2e9a9937bf6db55e41fea8297c6d02f83 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:42:45 -0400 Subject: [PATCH 01/18] Bumped client version(s) (#10750) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1ca7de0e135..994aba73292 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.2", + "version": "2024.8.3", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index e7496714416..ab46091790b 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.8.2", + "version": "2024.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.8.2", + "version": "2024.8.3", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 577683e0a86..df1f7f70d41 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.2", + "version": "2024.8.3", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 941839c6f41..68873cc8314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.8.2", + "version": "2024.8.3", "hasInstallScript": true, "license": "GPL-3.0" }, From 3fc1b5731cb7c43d70a870a3d5147c923a008478 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:59:30 -0400 Subject: [PATCH 02/18] Correct `dist:safari` Build (#10748) --- apps/browser/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index db743b509bf..433ddecd2ac 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -20,8 +20,8 @@ "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:opera": "npm run build:prod && gulp dist:opera", - "dist:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist:safari", - "dist:safari:mv3": "npm run build:prod && gulp dist:safari", + "dist:safari": "cross-env BROWSER=safari npm run build:prod && gulp dist:safari", + "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 BROWSER=safari run build:prod && cross-env MANIFEST_VERSION=3 BROWSER=safari gulp dist:safari", "dist:safari:mas": "npm run build:prod && gulp dist:safari:mas", "dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev", "dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg", From 11eba8d77902c8b115da541cc09a0472cf74aca3 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:11:42 -0400 Subject: [PATCH 03/18] Add Null Check On `diskAccount` (#10550) --- libs/common/src/platform/services/state.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 70d2211a884..d46a5189a48 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -117,6 +117,13 @@ export class StateService< state.accounts = {}; } state.accounts[userId] = this.createAccount(); + + if (diskAccount == null) { + // Return early because we can't set the diskAccount.profile + // if diskAccount itself is null + return state; + } + state.accounts[userId].profile = diskAccount.profile; return state; }); From a7da5bb40f42312a839d034a3a9621c411f2b848 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:12:21 -0400 Subject: [PATCH 04/18] Remove Some Unneccesary Async From Init Methods (#10485) --- .../src/autofill/background/notification.background.spec.ts | 4 ++-- .../src/autofill/background/notification.background.ts | 2 +- apps/browser/src/background/commands.background.ts | 2 +- apps/browser/src/background/idle.background.ts | 2 +- apps/browser/src/background/main.background.ts | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 2c9232d8b5c..f3ebe5b1cc9 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -137,8 +137,8 @@ describe("NotificationBackground", () => { }); describe("notification bar extension message handlers", () => { - beforeEach(async () => { - await notificationBackground.init(); + beforeEach(() => { + notificationBackground.init(); }); it("ignores messages whose command does not match the expected handlers", () => { diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 25f45bd0659..9aac9b099a2 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -94,7 +94,7 @@ export default class NotificationBackground { private accountService: AccountService, ) {} - async init() { + init() { if (chrome.runtime == null) { return; } diff --git a/apps/browser/src/background/commands.background.ts b/apps/browser/src/background/commands.background.ts index b88f6127bc4..b87f5d0fcfb 100644 --- a/apps/browser/src/background/commands.background.ts +++ b/apps/browser/src/background/commands.background.ts @@ -26,7 +26,7 @@ export default class CommandsBackground { this.isVivaldi = this.platformUtilsService.isVivaldi(); } - async init() { + init() { BrowserApi.messageListener("commands.background", (msg: any) => { if (msg.command === "unlockCompleted" && msg.data.target === "commands.background") { this.processCommand( diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index c0cd3a86aae..a5d50e8508f 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -23,7 +23,7 @@ export default class IdleBackground { this.idle = chrome.idle || (browser != null ? browser.idle : null); } - async init() { + init() { if (!this.idle) { return; } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3944f2d8afc..432d6566dda 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1238,11 +1238,11 @@ export default class MainBackground { await this.vaultTimeoutService.init(true); this.fido2Background.init(); await this.runtimeBackground.init(); - await this.notificationBackground.init(); + this.notificationBackground.init(); this.filelessImporterBackground.init(); - await this.commandsBackground.init(); + this.commandsBackground.init(); this.contextMenusBackground?.init(); - await this.idleBackground.init(); + this.idleBackground.init(); this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); await this.autoSubmitLoginBackground.init(); From e255d84121b27dee5afcb45cff44d1d1d74c39e6 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:18:02 -0700 Subject: [PATCH 05/18] [PM-4473] use anon layout for send access component (#10699) * use anon layout for send access component * fix width on explainer. * don't show creator ID until send is decrypted in send access --- apps/web/src/app/oss-routing.module.ts | 24 +++- .../src/app/tools/send/access.component.html | 127 +++++++----------- .../src/app/tools/send/access.component.ts | 11 ++ .../send/send-access-explainer.component.html | 18 +++ .../send/send-access-explainer.component.ts | 17 +++ apps/web/src/locales/en/messages.json | 4 + 6 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 apps/web/src/app/tools/send/send-access-explainer.component.html create mode 100644 apps/web/src/app/tools/send/send-access-explainer.component.ts diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 32dcb695a8f..de0e8a2da93 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -68,6 +68,7 @@ import { PreferencesComponent } from "./settings/preferences.component"; import { GeneratorComponent } from "./tools/generator.component"; import { ReportsModule } from "./tools/reports"; import { AccessComponent } from "./tools/send/access.component"; +import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component"; import { SendComponent } from "./tools/send/send.component"; import { VaultModule } from "./vault/individual-vault/vault.module"; @@ -145,11 +146,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn()], data: { titleId: "deleteAccount" } satisfies DataProperties, }, - { - path: "send/:sendId/:key", - component: AccessComponent, - data: { titleId: "Bitwarden Send" } satisfies DataProperties, - }, { path: "update-temp-password", component: UpdateTempPasswordComponent, @@ -210,6 +206,24 @@ const routes: Routes = [ }, ], }, + { + path: "send/:sendId/:key", + data: { + pageTitle: "viewSend", + showReadonlyHostname: true, + } satisfies DataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: AccessComponent, + }, + { + path: "", + outlet: "secondary", + component: SendAccessExplainerComponent, + }, + ], + }, { path: "set-password-jit", canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], diff --git a/apps/web/src/app/tools/send/access.component.html b/apps/web/src/app/tools/send/access.component.html index d1a6f4d42d2..2a7514ed854 100644 --- a/apps/web/src/app/tools/send/access.component.html +++ b/apps/web/src/app/tools/send/access.component.html @@ -1,85 +1,52 @@
-
- -
-

View Send

-
-
-

{{ "sendAccessCreatorIdentifier" | i18n: creatorIdentifier }}

-
- - {{ "viewSendHiddenEmailWarning" | i18n }} - {{ - "learnMore" | i18n - }}. - -
- - - - {{ "sendAccessUnavailable" | i18n }} - - - {{ "unexpectedErrorSend" | i18n }} - -
-

- {{ send.name }} -

-
- - - - - - - - -

- Expires: {{ expirationDate | date: "medium" }} -

-
+ + {{ "viewSendHiddenEmailWarning" | i18n }} + {{ + "learnMore" | i18n + }}. + + + + + {{ "sendAccessUnavailable" | i18n }} + + + {{ "unexpectedErrorSend" | i18n }} + +
+

+ {{ send.name }} +

+
+ + + - -
- - {{ "loading" | i18n }} -
-
-
-
-

- {{ "sendAccessTaglineProductDesc" | i18n }} - {{ "sendAccessTaglineLearnMore" | i18n }} - Bitwarden Send - {{ "sendAccessTaglineOr" | i18n }} - {{ - "sendAccessTaglineSignUp" | i18n - }} - {{ "sendAccessTaglineTryToday" | i18n }} + + + + +

+ Expires: {{ expirationDate | date: "medium" }}

-
+ + +
+ + {{ "loading" | i18n }} +
+
diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index f553542a619..07697ee8b37 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; +import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { RegisterRouteService } from "@bitwarden/auth/common"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -70,6 +71,7 @@ export class AccessComponent implements OnInit { private i18nService: I18nService, private configService: ConfigService, private registerRouteService: RegisterRouteService, + private layoutWrapperDataService: AnonLayoutWrapperDataService, protected formBuilder: FormBuilder, ) {} @@ -151,6 +153,15 @@ export class AccessComponent implements OnInit { !this.passwordRequired && !this.loading && !this.unavailable; + + if (this.creatorIdentifier != null) { + this.layoutWrapperDataService.setAnonLayoutWrapperData({ + pageSubtitle: { + subtitle: this.i18nService.t("sendAccessCreatorIdentifier", this.creatorIdentifier), + translate: false, + }, + }); + } }; protected setPassword(password: string) { diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.html b/apps/web/src/app/tools/send/send-access-explainer.component.html new file mode 100644 index 00000000000..e8090cb850c --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-explainer.component.html @@ -0,0 +1,18 @@ +
+

+ {{ "sendAccessTaglineProductDesc" | i18n }} + {{ "sendAccessTaglineLearnMore" | i18n }} + Bitwarden Send + {{ "sendAccessTaglineOr" | i18n }} + {{ + "sendAccessTaglineSignUp" | i18n + }} + {{ "sendAccessTaglineTryToday" | i18n }} +

+
diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.ts b/apps/web/src/app/tools/send/send-access-explainer.component.ts new file mode 100644 index 00000000000..756a1068985 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-explainer.component.ts @@ -0,0 +1,17 @@ +import { Component } from "@angular/core"; + +import { RegisterRouteService } from "@bitwarden/auth/common"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-explainer", + templateUrl: "send-access-explainer.component.html", + standalone: true, + imports: [SharedModule], +}) +export class SendAccessExplainerComponent { + // TODO: remove when email verification flag is removed + registerRoute$ = this.registerRouteService.registerRoute$(); + constructor(private registerRouteService: RegisterRouteService) {} +} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 40e4789923e..29e2e398254 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4983,6 +4983,10 @@ } } }, + "viewSend": { + "message": "View Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." From 9041a4cd4cd44a3f1741f45c357df4e4efca6002 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:33:58 -0700 Subject: [PATCH 06/18] [PM-6564] migrate auth toasts to CL toastService (#10665) * migrate auth toasts to CL toastService * fix component args * fix component args * fix specs * fix toastService args --- .../src/auth/popup/environment.component.ts | 4 +- apps/browser/src/auth/popup/hint.component.ts | 12 ++- apps/browser/src/auth/popup/home.component.ts | 12 +-- apps/browser/src/auth/popup/lock.component.ts | 4 +- .../popup/login-via-auth-request.component.ts | 3 + .../browser/src/auth/popup/login.component.ts | 3 + .../src/auth/popup/register.component.ts | 4 +- .../settings/account-security.component.ts | 33 ++++---- apps/browser/src/auth/popup/sso.component.ts | 3 + .../auth/popup/two-factor-auth.component.ts | 3 + .../auth/accessibility-cookie.component.ts | 32 +++---- .../src/auth/delete-account.component.ts | 12 +-- .../desktop/src/auth/environment.component.ts | 4 +- apps/desktop/src/auth/hint.component.ts | 12 ++- apps/desktop/src/auth/lock.component.spec.ts | 8 +- apps/desktop/src/auth/lock.component.ts | 4 +- .../auth/login/login-approval.component.ts | 42 +++++----- .../login/login-via-auth-request.component.ts | 3 + .../desktop/src/auth/login/login.component.ts | 13 +-- apps/desktop/src/auth/register.component.ts | 4 +- .../src/auth/set-password.component.ts | 4 +- apps/desktop/src/auth/sso.component.ts | 3 + apps/web/src/app/auth/hint.component.ts | 12 ++- .../web/src/app/auth/login/login.component.ts | 3 + .../migrate-legacy-encryption.component.ts | 14 ++-- .../src/app/auth/recover-delete.component.ts | 12 +-- .../app/auth/recover-two-factor.component.ts | 12 +-- .../register-form/register-form.component.ts | 14 ++-- .../account/change-avatar-dialog.component.ts | 15 +++- .../account/change-email.component.ts | 12 +-- .../account/deauthorize-sessions.component.ts | 12 +-- .../delete-account-dialog.component.ts | 13 +-- .../settings/account/profile.component.ts | 9 +- .../settings/change-password.component.ts | 50 ++++++----- .../emergency-access-add-edit.component.ts | 16 ++-- .../emergency-access.component.ts | 83 ++++++++++--------- .../emergency-access-takeover.component.ts | 14 ++-- .../change-kdf-confirmation.component.ts | 12 +-- .../two-factor-authenticator.component.ts | 10 ++- .../settings/two-factor-base.component.ts | 15 +++- .../auth/settings/two-factor-duo.component.ts | 4 +- .../settings/two-factor-email.component.ts | 4 +- .../settings/two-factor-webauthn.component.ts | 4 +- .../settings/two-factor-yubikey.component.ts | 3 +- .../auth/settings/verify-email.component.ts | 25 ++++-- .../create-credential-dialog.component.ts | 33 ++++---- .../delete-credential-dialog.component.ts | 19 +++-- .../user-verification-prompt.component.ts | 13 ++- apps/web/src/app/auth/sso.component.ts | 3 + .../src/app/auth/two-factor-auth.component.ts | 10 ++- .../app/auth/verify-email-token.component.ts | 14 +++- .../auth/verify-recover-delete.component.ts | 12 +-- .../bit-web/src/app/auth/sso/sso.component.ts | 10 ++- ...base-login-decryption-options.component.ts | 12 +-- .../components/captcha-protected.component.ts | 14 +++- .../components/change-password.component.ts | 43 +++++----- .../auth/components/environment.component.ts | 8 +- .../src/auth/components/hint.component.ts | 28 ++++--- .../src/auth/components/lock.component.ts | 63 +++++++------- .../login-via-auth-request.component.ts | 28 +++++-- .../src/auth/components/login.component.ts | 20 +++-- .../src/auth/components/register.component.ts | 41 +++++---- .../components/remove-password.component.ts | 31 +++++-- .../auth/components/set-password.component.ts | 16 +++- .../src/auth/components/sso.component.spec.ts | 22 +++-- .../src/auth/components/sso.component.ts | 22 ++--- .../two-factor-auth-email.component.ts | 22 ++--- .../two-factor-auth-webauthn.component.ts | 12 +-- .../two-factor-auth.component.spec.ts | 5 +- .../two-factor-auth.component.ts | 24 +++--- .../auth/components/two-factor.component.ts | 48 ++++++----- .../components/update-password.component.ts | 30 ++++--- .../update-temp-password.component.ts | 14 ++-- .../user-verification-prompt.component.ts | 8 +- .../user-verification-dialog.component.ts | 24 ++++-- 75 files changed, 782 insertions(+), 457 deletions(-) diff --git a/apps/browser/src/auth/popup/environment.component.ts b/apps/browser/src/auth/popup/environment.component.ts index ed348e563b6..b84f03b5fd7 100644 --- a/apps/browser/src/auth/popup/environment.component.ts +++ b/apps/browser/src/auth/popup/environment.component.ts @@ -5,6 +5,7 @@ import { EnvironmentComponent as BaseEnvironmentComponent } from "@bitwarden/ang import { ModalService } from "@bitwarden/angular/services/modal.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; @@ -21,8 +22,9 @@ export class EnvironmentComponent extends BaseEnvironmentComponent implements On i18nService: I18nService, private router: Router, modalService: ModalService, + toastService: ToastService, ) { - super(platformUtilsService, environmentService, i18nService, modalService); + super(platformUtilsService, environmentService, i18nService, modalService, toastService); this.showCustom = true; } diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index 214a43efb71..bc1f68f4c43 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -21,8 +22,17 @@ export class HintComponent extends BaseHintComponent { logService: LogService, private route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, + toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); super.onSuccessfulSubmit = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 43f8f3dcf4c..505931ad0f1 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -7,6 +7,7 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { AccountSwitcherService } from "./account-switching/services/account-switcher.service"; @@ -36,6 +37,7 @@ export class HomeComponent implements OnInit, OnDestroy { private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, private registerRouteService: RegisterRouteService, + private toastService: ToastService, ) {} async ngOnInit(): Promise { @@ -76,11 +78,11 @@ export class HomeComponent implements OnInit, OnDestroy { this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("invalidEmail"), + }); return; } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index a6da98fe996..f5413e4bea4 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -27,7 +27,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; @@ -72,6 +72,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { accountService: AccountService, kdfConfigService: KdfConfigService, syncService: SyncService, + toastService: ToastService, ) { super( masterPasswordService, @@ -100,6 +101,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { authService, kdfConfigService, syncService, + toastService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index f83062e6c97..53f29badee6 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -22,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -50,6 +51,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, private location: Location, + toastService: ToastService, ) { super( router, @@ -70,6 +72,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustService, authRequestService, loginStrategyService, + toastService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 79a02ede85d..6e73199969a 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -22,6 +22,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../platform/flags"; @@ -53,6 +54,7 @@ export class LoginComponent extends BaseLoginComponent { ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -74,6 +76,7 @@ export class LoginComponent extends BaseLoginComponent { ssoLoginService, webAuthnLoginService, registerRouteService, + toastService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index 61e007ac52a..dab1e62f850 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -13,7 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -39,6 +39,7 @@ export class RegisterComponent extends BaseRegisterComponent { logService: LogService, auditService: AuditService, dialogService: DialogService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -55,6 +56,7 @@ export class RegisterComponent extends BaseRegisterComponent { logService, auditService, dialogService, + toastService, ); } } diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 7bced79a0a8..25401f06f38 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -39,7 +39,7 @@ import { VaultTimeoutOption, VaultTimeoutStringType, } from "@bitwarden/common/types/vault-timeout.type"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -95,6 +95,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, private biometricStateService: BiometricStateService, + private toastService: ToastService, private biometricsService: BiometricsService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); @@ -274,11 +275,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { // The minTimeoutError does not apply to browser because it supports Immediately // So only check for the policyError if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("vaultTimeoutTooLarge"), + }); return; } @@ -315,11 +316,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { } if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("vaultTimeoutTooLarge"), + }); return; } @@ -417,11 +418,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { this.form.controls.biometric.setValue(result); if (!result) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorEnableBiometricTitle"), - this.i18nService.t("errorEnableBiometricDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorEnableBiometricTitle"), + message: this.i18nService.t("errorEnableBiometricDesc"), + }); } } catch (e) { // prevent duplicate dialog diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 33284717ab5..42222c42b97 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -22,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -51,6 +52,7 @@ export class SsoComponent extends BaseSsoComponent { accountService: AccountService, private authService: AuthService, @Inject(WINDOW) private win: Window, + toastService: ToastService, ) { super( ssoLoginService, @@ -69,6 +71,7 @@ export class SsoComponent extends BaseSsoComponent { configService, masterPasswordService, accountService, + toastService, ); environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index ea0d4a48e55..27c95321100 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -32,6 +32,7 @@ import { LinkModule, TypographyModule, DialogService, + ToastService, } from "@bitwarden/components"; import { @@ -95,6 +96,7 @@ export class TwoFactorAuthComponent @Inject(WINDOW) protected win: Window, private syncService: SyncService, private messagingService: MessagingService, + toastService: ToastService, ) { super( loginStrategyService, @@ -114,6 +116,7 @@ export class TwoFactorAuthComponent accountService, formBuilder, win, + toastService, ); super.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); diff --git a/apps/desktop/src/auth/accessibility-cookie.component.ts b/apps/desktop/src/auth/accessibility-cookie.component.ts index fc72b1a9d77..697bbcc88ec 100644 --- a/apps/desktop/src/auth/accessibility-cookie.component.ts +++ b/apps/desktop/src/auth/accessibility-cookie.component.ts @@ -6,6 +6,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-accessibility-cookie", @@ -25,6 +26,7 @@ export class AccessibilityCookieComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, protected ngZone: NgZone, + private toastService: ToastService, ) {} registerhCaptcha() { @@ -42,28 +44,28 @@ export class AccessibilityCookieComponent { } onCookieSavedSuccess() { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("accessibilityCookieSaved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("accessibilityCookieSaved"), + }); } onCookieSavedFailure() { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("noAccessibilityCookieSaved"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("noAccessibilityCookieSaved"), + }); } async submit() { if (Utils.getHostname(this.accessibilityForm.value.link) !== "accounts.hcaptcha.com") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidUrl"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidUrl"), + }); return; } this.listenForCookie = true; diff --git a/apps/desktop/src/auth/delete-account.component.ts b/apps/desktop/src/auth/delete-account.component.ts index a473310d386..0cb2bdd79ee 100644 --- a/apps/desktop/src/auth/delete-account.component.ts +++ b/apps/desktop/src/auth/delete-account.component.ts @@ -13,6 +13,7 @@ import { CalloutModule, DialogModule, DialogService, + ToastService, } from "@bitwarden/components"; import { UserVerificationComponent } from "../app/components/user-verification.component"; @@ -41,6 +42,7 @@ export class DeleteAccountComponent { private platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, private accountApiService: AccountApiService, + private toastService: ToastService, ) {} static open(dialogService: DialogService): DialogRef { @@ -54,10 +56,10 @@ export class DeleteAccountComponent { submit = async () => { const verification = this.deleteForm.get("verification").value; await this.accountApiService.deleteAccount(verification); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); }; } diff --git a/apps/desktop/src/auth/environment.component.ts b/apps/desktop/src/auth/environment.component.ts index a4b137a9e51..d24e0f86abe 100644 --- a/apps/desktop/src/auth/environment.component.ts +++ b/apps/desktop/src/auth/environment.component.ts @@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-environment", @@ -16,7 +17,8 @@ export class EnvironmentComponent extends BaseEnvironmentComponent { environmentService: EnvironmentService, i18nService: I18nService, modalService: ModalService, + toastService: ToastService, ) { - super(platformUtilsService, environmentService, i18nService, modalService); + super(platformUtilsService, environmentService, i18nService, modalService, toastService); } } diff --git a/apps/desktop/src/auth/hint.component.ts b/apps/desktop/src/auth/hint.component.ts index cee1f189817..34457029257 100644 --- a/apps/desktop/src/auth/hint.component.ts +++ b/apps/desktop/src/auth/hint.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -20,7 +21,16 @@ export class HintComponent extends BaseHintComponent { apiService: ApiService, logService: LogService, loginEmailService: LoginEmailServiceAbstraction, + toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); } } diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c5b5b7acf00..d81f2a486f6 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -34,7 +34,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricsService } from "src/platform/main/biometric"; @@ -62,6 +62,7 @@ describe("LockComponent", () => { let platformUtilsServiceMock: MockProxy; let activatedRouteMock: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; + let mockToastService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -72,6 +73,7 @@ describe("LockComponent", () => { messagingServiceMock = mock(); broadcasterServiceMock = mock(); platformUtilsServiceMock = mock(); + mockToastService = mock(); activatedRouteMock = mock(); activatedRouteMock.queryParams = mock(); @@ -187,6 +189,10 @@ describe("LockComponent", () => { provide: SyncService, useValue: mock(), }, + { + provide: ToastService, + useValue: mockToastService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 55cc79e0a68..350512b0f31 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -28,7 +28,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; const BroadcasterSubscriptionId = "LockComponent"; @@ -72,6 +72,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro authService: AuthService, kdfConfigService: KdfConfigService, syncService: SyncService, + toastService: ToastService, ) { super( masterPasswordService, @@ -100,6 +101,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro authService, kdfConfigService, syncService, + toastService, ); } diff --git a/apps/desktop/src/auth/login/login-approval.component.ts b/apps/desktop/src/auth/login/login-approval.component.ts index 296efb50e4a..39876f2945f 100644 --- a/apps/desktop/src/auth/login/login-approval.component.ts +++ b/apps/desktop/src/auth/login/login-approval.component.ts @@ -18,6 +18,7 @@ import { ButtonModule, DialogModule, DialogService, + ToastService, } from "@bitwarden/components"; const RequestTimeOut = 60000 * 15; //15 Minutes @@ -54,6 +55,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { protected appIdService: AppIdService, protected cryptoService: CryptoService, private dialogRef: DialogRef, + private toastService: ToastService, ) { this.notificationId = params.notificationId; } @@ -117,11 +119,11 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { private async retrieveAuthRequestAndRespond(approve: boolean) { this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) { - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("thisRequestIsNoLongerValid"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("thisRequestIsNoLongerValid"), + }); } else { const loginResponse = await this.authRequestService.approveOrDenyAuthRequest( approve, @@ -133,21 +135,21 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { showResultToast(loginResponse: AuthRequestResponse) { if (loginResponse.requestApproved) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t( + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( "logInConfirmedForEmailOnDevice", this.email, loginResponse.requestDeviceType, ), - ); + }); } else { - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("youDeniedALogInAttemptFromAnotherDevice"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("youDeniedALogInAttemptFromAnotherDevice"), + }); } } @@ -186,11 +188,11 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { } else { clearInterval(this.interval); this.dialogRef.close(); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("loginRequestHasAlreadyExpired"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("loginRequestHasAlreadyExpired"), + }); } } } diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index 7a8dfcbcda7..c0a6a51b907 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -23,6 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; @@ -58,6 +59,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, private location: Location, + toastService: ToastService, ) { super( router, @@ -78,6 +80,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustService, authRequestService, loginStrategyService, + toastService, ); super.onSuccessfulLogin = () => { diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index 68b25b8b7e9..2b5910baa96 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -25,6 +25,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; @@ -74,6 +75,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -95,6 +97,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest ssoLoginService, webAuthnLoginService, registerRouteService, + toastService, ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); @@ -162,11 +165,11 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest async continue() { await super.validateEmail(); if (!this.formGroup.controls.email.valid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("invalidEmail"), + }); return; } this.focusInput(); diff --git a/apps/desktop/src/auth/register.component.ts b/apps/desktop/src/auth/register.component.ts index be44c276485..e7c2cfd32b3 100644 --- a/apps/desktop/src/auth/register.component.ts +++ b/apps/desktop/src/auth/register.component.ts @@ -14,7 +14,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "RegisterComponent"; @@ -41,6 +41,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit, logService: LogService, auditService: AuditService, dialogService: DialogService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -57,6 +58,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit, logService, auditService, dialogService, + toastService, ); } diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index f14434b277b..28f1f69a598 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -22,7 +22,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "SetPasswordComponent"; @@ -56,6 +56,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService: DialogService, kdfConfigService: KdfConfigService, encryptService: EncryptService, + toastService: ToastService, ) { super( accountService, @@ -79,6 +80,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService, kdfConfigService, encryptService, + toastService, ); } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 234ebc85cee..6821a548945 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -18,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -43,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( ssoLoginService, @@ -61,6 +63,7 @@ export class SsoComponent extends BaseSsoComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts index 944b386e277..42744546234 100644 --- a/apps/web/src/app/auth/hint.component.ts +++ b/apps/web/src/app/auth/hint.component.ts @@ -8,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -30,8 +31,17 @@ export class HintComponent extends BaseHintComponent implements OnInit { logService: LogService, loginEmailService: LoginEmailServiceAbstraction, private formBuilder: FormBuilder, + protected toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); } ngOnInit(): void { diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 145d7666273..1422a7c1239 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -29,6 +29,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../../utils/flags"; @@ -71,6 +72,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -92,6 +94,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ssoLoginService, webAuthnLoginService, registerRouteService, + toastService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.showPasswordless = flagEnabled("showPasswordless"); diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 6695039307e..68eaae618fd 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -9,6 +9,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { SharedModule } from "../../shared"; import { UserKeyRotationModule } from "../key-rotation/user-key-rotation.module"; @@ -35,6 +36,7 @@ export class MigrateFromLegacyEncryptionComponent { private messagingService: MessagingService, private logService: LogService, private syncService: SyncService, + private toastService: ToastService, ) {} submit = async () => { @@ -59,12 +61,12 @@ export class MigrateFromLegacyEncryptionComponent { await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword, activeUser); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("keyUpdated"), - this.i18nService.t("logBackInOthersToo"), - { timeout: 15000 }, - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("keyUpdated"), + message: this.i18nService.t("logBackInOthersToo"), + timeout: 15000, + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/recover-delete.component.ts b/apps/web/src/app/auth/recover-delete.component.ts index 96afd910598..04c3eb1df25 100644 --- a/apps/web/src/app/auth/recover-delete.component.ts +++ b/apps/web/src/app/auth/recover-delete.component.ts @@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeleteRecoverRequest } from "@bitwarden/common/models/request/delete-recover.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-delete", @@ -25,6 +26,7 @@ export class RecoverDeleteComponent { private apiService: ApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} submit = async () => { @@ -35,11 +37,11 @@ export class RecoverDeleteComponent { const request = new DeleteRecoverRequest(); request.email = this.email.value.trim().toLowerCase(); await this.apiService.postAccountRecoverDelete(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deleteRecoverEmailSent"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deleteRecoverEmailSent"), + }); await this.router.navigate(["/"]); }; diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 28296aa89d5..0774a9c777a 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -8,6 +8,7 @@ import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-two-factor", @@ -27,6 +28,7 @@ export class RecoverTwoFactorComponent { private i18nService: I18nService, private cryptoService: CryptoService, private loginStrategyService: LoginStrategyServiceAbstraction, + private toastService: ToastService, ) {} get email(): string { @@ -53,11 +55,11 @@ export class RecoverTwoFactorComponent { const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email); request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key); await this.apiService.postTwoFactorRecover(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("twoStepRecoverDisabled"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepRecoverDisabled"), + }); await this.router.navigate(["/"]); }; } diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index 693cdb4d6c0..bf4a3e8203f 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; @@ -52,6 +52,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn auditService: AuditService, dialogService: DialogService, acceptOrgInviteService: AcceptOrganizationInviteService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -68,6 +69,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn logService, auditService, dialogService, + toastService, ); super.modifyRegisterRequest = async (request: RegisterRequest) => { // Org invites are deep linked. Non-existent accounts are redirected to the register page. @@ -104,11 +106,11 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn this.enforcedPolicyOptions, ) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), + }); return; } diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index b555faf9a12..e20245bfa00 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -15,7 +15,7 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; type ChangeAvatarDialogData = { profile: ProfileResponse; @@ -55,6 +55,7 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private avatarService: AvatarService, private dialogRef: DialogRef, + private toastService: ToastService, ) { this.profile = data.profile; } @@ -93,9 +94,17 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) { await this.avatarService.setAvatarColor(this.currentSelection); this.dialogRef.close(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("avatarUpdated"), + }); } else { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } }; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index e5a3c72337b..ac493357765 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -12,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-change-email", @@ -39,6 +40,7 @@ export class ChangeEmailComponent implements OnInit { private stateService: StateService, private formBuilder: FormBuilder, private kdfConfigService: KdfConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -100,11 +102,11 @@ export class ChangeEmailComponent implements OnInit { try { await this.apiService.postEmail(request); this.reset(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("emailChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("emailChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index 9b9ba7eb795..dcaf38ee29e 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-deauthorize-sessions", @@ -23,6 +24,7 @@ export class DeauthorizeSessionsComponent { private userVerificationService: UserVerificationService, private messagingService: MessagingService, private logService: LogService, + private toastService: ToastService, ) {} async submit() { @@ -31,11 +33,11 @@ export class DeauthorizeSessionsComponent { .buildRequest(this.masterPassword) .then((request) => this.apiService.postSecurityStamp(request)); await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("sessionsDeauthorized"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("sessionsDeauthorized"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index b3dd8fbe616..c7c67416e18 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -7,7 +7,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ templateUrl: "delete-account-dialog.component.html", @@ -24,6 +24,7 @@ export class DeleteAccountDialogComponent { private formBuilder: FormBuilder, private accountApiService: AccountApiService, private dialogRef: DialogRef, + private toastService: ToastService, ) {} submit = async () => { @@ -31,11 +32,11 @@ export class DeleteAccountDialogComponent { const verification = this.deleteForm.get("verification").value; await this.accountApiService.deleteAccount(verification); this.dialogRef.close(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); } catch (e) { if (e instanceof ErrorResponse && e.statusCode === 400) { this.invalidSecret = true; diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index a960adfe5de..8b659e579da 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -8,7 +8,7 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; @@ -33,6 +33,7 @@ export class ProfileComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private stateService: StateService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -64,6 +65,10 @@ export class ProfileComponent implements OnInit, OnDestroy { submit = async () => { const request = new UpdateProfileRequest(this.formGroup.get("name").value); await this.apiService.putProfile(request); - this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("accountUpdated"), + }); }; } diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 5279c05032a..2cc7c101d0b 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -22,7 +22,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { UserKeyRotationService } from "../key-rotation/user-key-rotation.service"; @@ -60,6 +60,7 @@ export class ChangePasswordComponent kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( i18nService, @@ -73,6 +74,7 @@ export class ChangePasswordComponent kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -141,11 +143,11 @@ export class ChangePasswordComponent this.masterPasswordHint != null && this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase() ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("hintEqualsPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("hintEqualsPassword"), + }); return; } @@ -159,11 +161,11 @@ export class ChangePasswordComponent async setupSubmitActions() { if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } @@ -194,11 +196,11 @@ export class ChangePasswordComponent const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); if (userKey == null) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("invalidMasterPassword"), + }); return; } @@ -225,14 +227,18 @@ export class ChangePasswordComponent await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("masterPasswordChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("masterPasswordChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index d99c693e73e..fa5e80c81f5 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -5,7 +5,7 @@ import { FormBuilder, Validators } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; @@ -51,6 +51,7 @@ export class EmergencyAccessAddEditComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private logService: LogService, private dialogRef: DialogRef, + private toastService: ToastService, ) {} async ngOnInit() { this.editMode = this.loading = this.params.emergencyAccessId != null; @@ -104,11 +105,14 @@ export class EmergencyAccessAddEditComponent implements OnInit { this.addEditForm.value.waitTime, ); } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editMode ? "editedUserId" : "invitedUsers", + this.params.name, + ), + }); this.dialogRef.close(EmergencyAccessAddEditDialogResult.Saved); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 05e65405fb7..d8cedd5bd43 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -10,7 +10,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessStatusType } from "../../emergency-access/enums/emergency-access-status-type"; @@ -66,6 +66,7 @@ export class EmergencyAccessComponent implements OnInit { protected dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private toastService: ToastService, ) { this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } @@ -121,11 +122,11 @@ export class EmergencyAccessComponent implements OnInit { } this.actionPromise = this.emergencyAccessService.reinvite(contact.id); await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenReinvited", contact.email), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenReinvited", contact.email), + }); this.actionPromise = null; } @@ -153,11 +154,11 @@ export class EmergencyAccessComponent implements OnInit { if (result === EmergencyAccessConfirmDialogResult.Confirmed) { await this.emergencyAccessService.confirm(contact.id, contact.granteeId); updateUser(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), + }); } return; } @@ -166,11 +167,11 @@ export class EmergencyAccessComponent implements OnInit { await this.actionPromise; updateUser(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), + }); this.actionPromise = null; } @@ -187,11 +188,11 @@ export class EmergencyAccessComponent implements OnInit { try { await this.emergencyAccessService.delete(details.id); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.userNamePipe.transform(details)), + }); if (details instanceof GranteeEmergencyAccess) { this.removeGrantee(details); @@ -221,11 +222,11 @@ export class EmergencyAccessComponent implements OnInit { await this.emergencyAccessService.requestAccess(details.id); details.status = EmergencyAccessStatusType.RecoveryInitiated; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("requestSent", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("requestSent", this.userNamePipe.transform(details)), + }); } async approve(details: GranteeEmergencyAccess) { @@ -250,22 +251,22 @@ export class EmergencyAccessComponent implements OnInit { await this.emergencyAccessService.approve(details.id); details.status = EmergencyAccessStatusType.RecoveryApproved; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details)), + }); } async reject(details: GranteeEmergencyAccess) { await this.emergencyAccessService.reject(details.id); details.status = EmergencyAccessStatusType.Confirmed; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details)), + }); } takeover = async (details: GrantorEmergencyAccess) => { @@ -278,11 +279,11 @@ export class EmergencyAccessComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === EmergencyAccessTakeoverResultType.Done) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), + }); } }; diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index a3d856aa697..26995c7ce09 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -15,7 +15,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EmergencyAccessService } from "../../../emergency-access"; @@ -64,6 +64,7 @@ export class EmergencyAccessTakeoverComponent kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + protected toastService: ToastService, ) { super( i18nService, @@ -77,6 +78,7 @@ export class EmergencyAccessTakeoverComponent kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -114,11 +116,11 @@ export class EmergencyAccessTakeoverComponent ); } catch (e) { this.logService.error(e); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("unexpectedError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); } this.dialogRef.close(EmergencyAccessTakeoverResultType.Done); }; diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0c754e262e1..295037ce6b5 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -12,6 +12,7 @@ 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 { KdfType } from "@bitwarden/common/platform/enums"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-change-kdf-confirmation", @@ -35,6 +36,7 @@ export class ChangeKdfConfirmationComponent { private messagingService: MessagingService, @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, private accountService: AccountService, + private toastService: ToastService, ) { this.kdfConfig = params.kdfConfig; this.masterPassword = null; @@ -46,11 +48,11 @@ export class ChangeKdfConfirmationComponent { } this.loading = true; await this.makeKeyAndSaveAsync(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("encKeySettingsChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("encKeySettingsChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); this.loading = false; }; diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts index b4bef9f74e3..fdd595b7fcd 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -68,6 +68,7 @@ export class TwoFactorAuthenticatorComponent private accountService: AccountService, dialogService: DialogService, private configService: ConfigService, + protected toastService: ToastService, ) { super( apiService, @@ -76,6 +77,7 @@ export class TwoFactorAuthenticatorComponent logService, userVerificationService, dialogService, + toastService, ); this.qrScript = window.document.createElement("script"); this.qrScript.src = "scripts/qrious.min.js"; @@ -148,7 +150,11 @@ export class TwoFactorAuthenticatorComponent request.userVerificationToken = this.userVerificationToken; await this.apiService.deleteTwoFactorAuthenticator(request); this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } diff --git a/apps/web/src/app/auth/settings/two-factor-base.component.ts b/apps/web/src/app/auth/settings/two-factor-base.component.ts index ac8906641dd..2a6af1df98c 100644 --- a/apps/web/src/app/auth/settings/two-factor-base.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-base.component.ts @@ -10,7 +10,7 @@ import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export abstract class TwoFactorBaseComponent { @@ -33,6 +33,7 @@ export abstract class TwoFactorBaseComponent { protected logService: LogService, protected userVerificationService: UserVerificationService, protected dialogService: DialogService, + protected toastService: ToastService, ) {} protected auth(authResponse: AuthResponseBase) { @@ -76,7 +77,11 @@ export abstract class TwoFactorBaseComponent { } await promise; this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } catch (e) { this.logService.error(e); @@ -102,7 +107,11 @@ export abstract class TwoFactorBaseComponent { await this.apiService.putTwoFactorDisable(request); } this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.ts b/apps/web/src/app/auth/settings/two-factor-duo.component.ts index a211aa4c9ba..1a5b5917108 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.ts @@ -11,7 +11,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -40,6 +40,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnI dialogService: DialogService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + protected toastService: ToastService, ) { super( apiService, @@ -48,6 +49,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnI logService, userVerificationService, dialogService, + toastService, ); } diff --git a/apps/web/src/app/auth/settings/two-factor-email.component.ts b/apps/web/src/app/auth/settings/two-factor-email.component.ts index 96b4bfb38d5..524b00d114f 100644 --- a/apps/web/src/app/auth/settings/two-factor-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-email.component.ts @@ -14,7 +14,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -45,6 +45,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements O dialogService: DialogService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + protected toastService: ToastService, ) { super( apiService, @@ -53,6 +54,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements O logService, userVerificationService, dialogService, + toastService, ); } get token() { diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts index 5e8ea37e930..9aeafaf2c65 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts @@ -16,7 +16,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -61,6 +61,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { logService: LogService, userVerificationService: UserVerificationService, dialogService: DialogService, + toastService: ToastService, ) { super( apiService, @@ -69,6 +70,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { logService, userVerificationService, dialogService, + toastService, ); this.auth(data); } diff --git a/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts b/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts index 83718360cab..3b601084c35 100644 --- a/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts @@ -55,7 +55,7 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent implements userVerificationService: UserVerificationService, dialogService: DialogService, private formBuilder: FormBuilder, - private toastService: ToastService, + protected toastService: ToastService, ) { super( apiService, @@ -64,6 +64,7 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent implements logService, userVerificationService, dialogService, + toastService, ); } diff --git a/apps/web/src/app/auth/settings/verify-email.component.ts b/apps/web/src/app/auth/settings/verify-email.component.ts index e8809cd8931..6fa7a49bc08 100644 --- a/apps/web/src/app/auth/settings/verify-email.component.ts +++ b/apps/web/src/app/auth/settings/verify-email.component.ts @@ -6,7 +6,13 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { AsyncActionsModule, BannerModule, ButtonModule, LinkModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + BannerModule, + ButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; @Component({ standalone: true, @@ -25,22 +31,27 @@ export class VerifyEmailComponent { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private tokenService: TokenService, + private toastService: ToastService, ) {} async verifyEmail(): Promise { await this.apiService.refreshIdentityToken(); if (await this.tokenService.getEmailVerified()) { this.onVerified.emit(true); - this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emailVerified"), + }); return; } await this.apiService.postAccountVerifyEmail(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("checkInboxForVerification"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("checkInboxForVerification"), + }); } send = async () => { diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index fd72cbbb711..c0ed678d0a7 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -9,7 +9,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebauthnLoginAdminService } from "../../../core"; import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view"; @@ -60,6 +60,7 @@ export class CreateCredentialDialogComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private logService: LogService, + private toastService: ToastService, ) {} ngOnInit(): void { @@ -102,11 +103,11 @@ export class CreateCredentialDialogComponent implements OnInit { this.invalidSecret = true; } else { this.logService?.error(error); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("unexpectedError"), - error.message, - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("unexpectedError"), + message: error.message, + }); } return; } @@ -162,17 +163,17 @@ export class CreateCredentialDialogComponent implements OnInit { ); if (await firstValueFrom(this.hasPasskeys$)) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("passkeySaved", name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passkeySaved", name), + }); } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("loginWithPasskeyEnabled"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("loginWithPasskeyEnabled"), + }); } this.dialogRef.close(CreateCredentialDialogResult.Success); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts index 4fec10d2695..ce86874ce46 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts @@ -8,7 +8,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebauthnLoginAdminService } from "../../../core"; import { WebauthnLoginCredentialView } from "../../../core/views/webauthn-login-credential.view"; @@ -38,6 +38,7 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private logService: LogService, + private toastService: ToastService, ) {} ngOnInit(): void { @@ -55,17 +56,21 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy { this.dialogRef.disableClose = true; try { await this.webauthnService.deleteCredential(this.credential.id, this.formGroup.value.secret); - this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeyRemoved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passkeyRemoved"), + }); } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 400) { this.invalidSecret = true; } else { this.logService?.error(error); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("unexpectedError"), - error.message, - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("unexpectedError"), + message: error.message, + }); } return false; } finally { diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts index cd4ac2db356..7947d53c992 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts @@ -9,7 +9,7 @@ import { import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; /** * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent instead. @@ -25,8 +25,17 @@ export class UserVerificationPromptComponent extends BaseUserVerificationPrompt formBuilder: FormBuilder, platformUtilsService: PlatformUtilsService, i18nService: I18nService, + toastService: ToastService, ) { - super(null, data, userVerificationService, formBuilder, platformUtilsService, i18nService); + super( + null, + data, + userVerificationService, + formBuilder, + platformUtilsService, + i18nService, + toastService, + ); } override close(success: boolean) { diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 93a00054eb3..f55152fed3b 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -60,6 +61,7 @@ export class SsoComponent extends BaseSsoComponent implements OnInit { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( ssoLoginService, @@ -78,6 +80,7 @@ export class SsoComponent extends BaseSsoComponent implements OnInit { configService, masterPasswordService, accountService, + toastService, ); this.redirectUri = window.location.origin + "/sso-connector.html"; this.clientId = "web"; diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index fbdddecce98..18660b2ca63 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -17,7 +17,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { LinkModule, TypographyModule, CheckboxModule, DialogService } from "@bitwarden/components"; +import { + LinkModule, + TypographyModule, + CheckboxModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; @@ -81,6 +87,7 @@ export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent { accountService: AccountService, formBuilder: FormBuilder, @Inject(WINDOW) protected win: Window, + toastService: ToastService, ) { super( loginStrategyService, @@ -100,6 +107,7 @@ export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent { accountService, formBuilder, win, + toastService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 67ea7c9785a..d47e2c885ec 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-email-token", @@ -23,6 +24,7 @@ export class VerifyEmailTokenComponent implements OnInit { private apiService: ApiService, private logService: LogService, private stateService: StateService, + private toastService: ToastService, ) {} ngOnInit() { @@ -36,7 +38,11 @@ export class VerifyEmailTokenComponent implements OnInit { if (await this.stateService.getIsAuthenticated()) { await this.apiService.refreshIdentityToken(); } - this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emailVerified"), + }); // 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(["/"]); @@ -45,7 +51,11 @@ export class VerifyEmailTokenComponent implements OnInit { this.logService.error(e); } } - this.platformUtilsService.showToast("error", null, this.i18nService.t("emailVerifiedFailed")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("emailVerifiedFailed"), + }); // 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(["/"]); diff --git a/apps/web/src/app/auth/verify-recover-delete.component.ts b/apps/web/src/app/auth/verify-recover-delete.component.ts index b3d380fcbda..179913d7e32 100644 --- a/apps/web/src/app/auth/verify-recover-delete.component.ts +++ b/apps/web/src/app/auth/verify-recover-delete.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VerifyDeleteRecoverRequest } from "@bitwarden/common/models/request/verify-delete-recover.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-recover-delete", @@ -26,6 +27,7 @@ export class VerifyRecoverDeleteComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private route: ActivatedRoute, + private toastService: ToastService, ) {} ngOnInit() { @@ -44,11 +46,11 @@ export class VerifyRecoverDeleteComponent implements OnInit { submit = async () => { const request = new VerifyDeleteRecoverRequest(this.userId, this.token); await this.apiService.postAccountRecoverDeleteToken(request); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); await this.router.navigate(["/"]); }; } diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 4074c20d9df..e37da8c7840 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -26,9 +26,11 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { ToastService } from "@bitwarden/components"; import { ssoTypeValidator } from "./sso-type.validator"; @@ -189,6 +191,8 @@ export class SsoComponent implements OnInit, OnDestroy { private i18nService: I18nService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, + private configService: ConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -282,7 +286,11 @@ export class SsoComponent implements OnInit, OnDestroy { const response = await this.organizationApiService.updateSso(this.organizationId, request); this.populateForm(response); - this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("ssoSettingsSaved"), + }); }; async validateKeyConnectorUrl() { diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 0cc416a74bd..80088bf7f91 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -39,6 +39,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; enum State { NewUser, @@ -104,6 +105,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -275,11 +277,11 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); await this.apiService.postAccountKeys(keysRequest); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("accountSuccessfullyCreated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("accountSuccessfullyCreated"), + }); await this.passwordResetEnrollmentService.enroll(this.data.organizationId); diff --git a/libs/angular/src/auth/components/captcha-protected.component.ts b/libs/angular/src/auth/components/captcha-protected.component.ts index 0f549d24988..7186f6c3c48 100644 --- a/libs/angular/src/auth/components/captcha-protected.component.ts +++ b/libs/angular/src/auth/components/captcha-protected.component.ts @@ -6,6 +6,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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 { ToastService } from "@bitwarden/components"; @Directive() export abstract class CaptchaProtectedComponent { @@ -17,6 +18,7 @@ export abstract class CaptchaProtectedComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected toastService: ToastService, ) {} async setupCaptcha() { @@ -31,10 +33,18 @@ export abstract class CaptchaProtectedComponent { this.captchaToken = token; }, (error: string) => { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: error, + }); }, (info: string) => { - this.platformUtilsService.showToast("info", this.i18nService.t("info"), info); + this.toastService.showToast({ + variant: "info", + title: this.i18nService.t("info"), + message: info, + }); }, ); } diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index d6c0ec92710..45ddf9095fe 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -15,7 +15,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordColorText } from "../../tools/password-strength/password-strength.component"; @@ -49,6 +49,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected kdfConfigService: KdfConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -127,27 +128,27 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { async strongPassword(): Promise { if (this.masterPassword == null || this.masterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } if (this.masterPassword.length < this.minimumLength) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordMinimumlength", this.minimumLength), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength), + }); return false; } if (this.masterPassword !== this.masterPasswordRetype) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPassDoesntMatch"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPassDoesntMatch"), + }); return false; } @@ -161,11 +162,11 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { this.enforcedPolicyOptions, ) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), + }); return false; } diff --git a/libs/angular/src/auth/components/environment.component.ts b/libs/angular/src/auth/components/environment.component.ts index a58d5e5082e..25f10553308 100644 --- a/libs/angular/src/auth/components/environment.component.ts +++ b/libs/angular/src/auth/components/environment.component.ts @@ -7,6 +7,7 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { ModalService } from "../../services/modal.service"; @@ -27,6 +28,7 @@ export class EnvironmentComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, private modalService: ModalService, + private toastService: ToastService, ) { this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { if (env.getRegion() !== Region.SelfHosted) { @@ -59,7 +61,11 @@ export class EnvironmentComponent { notifications: this.notificationsUrl, }); - this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("environmentSaved"), + }); this.saved(); } diff --git a/libs/angular/src/auth/components/hint.component.ts b/libs/angular/src/auth/components/hint.component.ts index 484604b6a5a..7a152efbb9f 100644 --- a/libs/angular/src/auth/components/hint.component.ts +++ b/libs/angular/src/auth/components/hint.component.ts @@ -7,6 +7,7 @@ import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/passw import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Directive() export class HintComponent implements OnInit { @@ -23,6 +24,7 @@ export class HintComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, private logService: LogService, private loginEmailService: LoginEmailServiceAbstraction, + protected toastService: ToastService, ) {} ngOnInit(): void { @@ -31,26 +33,30 @@ export class HintComponent implements OnInit { async submit() { if (this.email == null || this.email === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("emailRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("emailRequired"), + }); return; } if (this.email.indexOf("@") === -1) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidEmail"), + }); return; } try { this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); await this.formPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("masterPassSent")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("masterPassSent"), + }); if (this.onSuccessfulSubmit != null) { this.onSuccessfulSubmit(); } else if (this.router != null) { diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 400dcfd8891..484e1c63469 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -36,7 +36,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export class LockComponent implements OnInit, OnDestroy { @@ -90,6 +90,7 @@ export class LockComponent implements OnInit, OnDestroy { protected authService: AuthService, protected kdfConfigService: KdfConfigService, protected syncService: SyncService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -167,11 +168,11 @@ export class LockComponent implements OnInit, OnDestroy { private async handlePinRequiredUnlock() { if (this.pin == null || this.pin === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("pinRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("pinRequired"), + }); return; } @@ -195,36 +196,36 @@ export class LockComponent implements OnInit, OnDestroy { // Log user out if they have entered an invalid PIN too many times if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), + }); this.messagingService.send("logout"); return; } - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidPin"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidPin"), + }); } catch { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("unexpectedError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); } } private async handleMasterPasswordRequiredUnlock() { if (this.masterPassword == null || this.masterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return; } await this.doUnlockWithMasterPassword(); @@ -258,11 +259,11 @@ export class LockComponent implements OnInit, OnDestroy { } if (!passwordValid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); return; } diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index f6356314f5b..452b5ceee1e 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -32,6 +32,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -88,8 +89,9 @@ export class LoginViaAuthRequestComponent private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); // TODO: I don't know why this is necessary. // Why would the existence of the email depend on the navigation? @@ -105,7 +107,11 @@ export class LoginViaAuthRequestComponent .subscribe((id) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => { - this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); this.logService.error("Failed to use approved auth request: " + e.message); }); }); @@ -135,7 +141,11 @@ export class LoginViaAuthRequestComponent const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (!this.email) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("userEmailMissing"), + }); // 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(["/login-initiated"]); @@ -158,7 +168,11 @@ export class LoginViaAuthRequestComponent this.email = this.loginEmailService.getEmail(); if (!this.email) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("userEmailMissing"), + }); // 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(["/login"]); @@ -402,7 +416,11 @@ export class LoginViaAuthRequestComponent // TODO: this should eventually be enforced via deleting this on the server once it is used await this.authRequestService.clearAdminAuthRequest(userId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("loginApproved"), + }); // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 40880b514aa..501d753a976 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -24,6 +24,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { @@ -92,8 +93,9 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, protected ssoLoginService: SsoLoginServiceAbstraction, protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, protected registerRouteService: RegisterRouteService, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); } async ngOnInit() { @@ -135,7 +137,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, //desktop, browser; This should be removed once all clients use reactive forms if (this.formGroup.invalid && showToast) { const errorText = this.getErrorToastMessage(); - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: errorText, + }); return; } @@ -327,11 +333,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, return false; } - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("encryptionKeyMigrationRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("encryptionKeyMigrationRequired"), + }); return true; } diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 366290a79ad..60adcba4d9e 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -17,7 +17,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { @@ -97,8 +97,9 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn protected logService: LogService, protected auditService: AuditService, protected dialogService: DialogService, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); this.showTerms = !platformUtilsService.isSelfHost(); this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength); } @@ -129,11 +130,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } if (this.isInTrialFlow) { if (!this.accountCreated) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("trialAccountCreated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("trialAccountCreated"), + }); } const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken); if (loginResponse.captchaRequired) { @@ -141,11 +142,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } this.createdAccount.emit(this.formGroup.value.email); } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("newAccountCreated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("newAccountCreated"), + }); // 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([this.successRoute], { queryParams: { email: email } }); @@ -210,11 +211,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn this.showErrorSummary = true; if (this.formGroup.get("acceptPolicies").hasError("required")) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("acceptPoliciesRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("acceptPoliciesRequired"), + }); return { isValid: false }; } @@ -226,7 +227,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn //desktop, browser if (this.formGroup.invalid && showToast) { const errorText = this.getErrorToastMessage(); - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: errorText, + }); return { isValid: false }; } diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/angular/src/auth/components/remove-password.component.ts index ad5027734c8..4b8e9cc52f0 100644 --- a/libs/angular/src/auth/components/remove-password.component.ts +++ b/libs/angular/src/auth/components/remove-password.component.ts @@ -9,7 +9,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export class RemovePasswordComponent implements OnInit { @@ -30,6 +30,7 @@ export class RemovePasswordComponent implements OnInit { private keyConnectorService: KeyConnectorService, private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -47,17 +48,21 @@ export class RemovePasswordComponent implements OnInit { try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedMasterPassword"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedMasterPassword"), + }); await this.keyConnectorService.removeConvertAccountRequired(); // 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([""]); } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e.message, + }); } }; @@ -76,13 +81,21 @@ export class RemovePasswordComponent implements OnInit { this.leaving = true; this.actionPromise = this.organizationApiService.leave(this.organization.id); await this.actionPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("leftOrganization"), + }); await this.keyConnectorService.removeConvertAccountRequired(); // 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([""]); } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e, + }); } }; } diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index f94a31e8d20..e9662c71076 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -32,7 +32,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -74,6 +74,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements dialogService: DialogService, kdfConfigService: KdfConfigService, private encryptService: EncryptService, + protected toastService: ToastService, ) { super( i18nService, @@ -87,6 +88,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -137,7 +139,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements ) .subscribe({ error: () => { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); }, }); } @@ -237,7 +243,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements this.router.navigate([this.successRoute]); } } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 8584abeeb85..af92c7dd1d4 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -28,6 +28,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { SsoComponent } from "./sso.component"; @@ -65,7 +66,7 @@ describe("SsoComponent", () => { let mockSsoLoginService: MockProxy; let mockStateService: MockProxy; - let mockPlatformUtilsService: MockProxy; + let mockToastService: MockProxy; let mockApiService: MockProxy; let mockCryptoFunctionService: MockProxy; let mockEnvironmentService: MockProxy; @@ -75,6 +76,7 @@ describe("SsoComponent", () => { let mockConfigService: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; + let mockPlatformUtilsService: MockProxy; // Mock authService.logIn params let code: string; @@ -117,7 +119,7 @@ describe("SsoComponent", () => { mockSsoLoginService = mock(); mockStateService = mock(); - mockPlatformUtilsService = mock(); + mockToastService = mock(); mockApiService = mock(); mockCryptoFunctionService = mock(); mockEnvironmentService = mock(); @@ -127,6 +129,7 @@ describe("SsoComponent", () => { mockConfigService = mock(); mockAccountService = mockAccountServiceWith(userId); mockMasterPasswordService = new FakeMasterPasswordService(); + mockPlatformUtilsService = mock(); // Mock loginStrategyService.logIn params code = "code"; @@ -196,7 +199,7 @@ describe("SsoComponent", () => { { provide: I18nService, useValue: mockI18nService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: StateService, useValue: mockStateService }, - { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: ToastService, useValue: mockToastService }, { provide: ApiService, useValue: mockApiService }, { provide: CryptoFunctionService, useValue: mockCryptoFunctionService }, @@ -214,6 +217,7 @@ describe("SsoComponent", () => { { provide: ConfigService, useValue: mockConfigService }, { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, ], }); @@ -594,12 +598,12 @@ describe("SsoComponent", () => { expect(mockLogService.error).toHaveBeenCalledTimes(1); expect(mockLogService.error).toHaveBeenCalledWith(error); - expect(mockPlatformUtilsService.showToast).toHaveBeenCalledTimes(1); - expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith( - "error", - null, - "ssoKeyConnectorError", - ); + expect(mockToastService.showToast).toHaveBeenCalledTimes(1); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: "ssoKeyConnectorError", + }); expect(mockRouter.navigate).not.toHaveBeenCalled(); }); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index cc105222c26..ac64ae02462 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -26,6 +26,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Directive() @@ -71,6 +72,7 @@ export class SsoComponent implements OnInit { protected configService: ConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -111,11 +113,11 @@ export class SsoComponent implements OnInit { async submit(returnUri?: string, includeUserIdentifier?: boolean) { if (this.identifier == null || this.identifier === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("ssoValidationFailed"), - this.i18nService.t("ssoIdentifierRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("ssoValidationFailed"), + message: this.i18nService.t("ssoIdentifierRequired"), + }); return; } @@ -382,11 +384,11 @@ export class SsoComponent implements OnInit { // TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here if (e.message === "Key Connector error") { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("ssoKeyConnectorError"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("ssoKeyConnectorError"), + }); } } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts index 5a1231daac6..72d7e6a76c8 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts @@ -20,6 +20,7 @@ import { TypographyModule, FormFieldModule, AsyncActionsModule, + ToastService, } from "@bitwarden/components"; @Component({ @@ -55,6 +56,7 @@ export class TwoFactorAuthEmailComponent implements OnInit { protected logService: LogService, protected apiService: ApiService, protected appIdService: AppIdService, + private toastService: ToastService, ) {} async ngOnInit(): Promise { @@ -74,11 +76,11 @@ export class TwoFactorAuthEmailComponent implements OnInit { } if ((await this.loginStrategyService.getEmail()) == null) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("sessionTimeout"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("sessionTimeout"), + }); return; } @@ -94,11 +96,11 @@ export class TwoFactorAuthEmailComponent implements OnInit { this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), + }); } } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts index 7f0554c1f0f..7e9f6486911 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts @@ -21,6 +21,7 @@ import { TypographyModule, FormFieldModule, AsyncActionsModule, + ToastService, } from "@bitwarden/components"; @Component({ @@ -56,6 +57,7 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { protected environmentService: EnvironmentService, protected twoFactorService: TwoFactorService, protected route: ActivatedRoute, + private toastService: ToastService, ) { this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -85,11 +87,11 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { this.token.emit(token); }, (error: string) => { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("webauthnCancelOrTimeout"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("webauthnCancelOrTimeout"), + }); }, (info: string) => { if (info === "ready") { diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts index 489ca5c1781..755813a677a 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts @@ -35,7 +35,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorAuthComponent } from "./two-factor-auth.component"; @@ -76,6 +76,7 @@ describe("TwoFactorComponent", () => { let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; let mockDialogService: MockProxy; + let mockToastService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -113,6 +114,7 @@ describe("TwoFactorComponent", () => { mockAccountService = mockAccountServiceWith(userId); mockMasterPasswordService = new FakeMasterPasswordService(); mockDialogService = mock(); + mockToastService = mock(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -193,6 +195,7 @@ describe("TwoFactorComponent", () => { { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, { provide: DialogService, useValue: mockDialogService }, + { provide: ToastService, useValue: mockToastService }, ], }); diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts index 21aaf119c43..58edeed93e9 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts @@ -34,6 +34,7 @@ import { ButtonModule, DialogService, FormFieldModule, + ToastService, } from "@bitwarden/components"; import { CaptchaProtectedComponent } from "../captcha-protected.component"; @@ -142,8 +143,9 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements private accountService: AccountService, private formBuilder: FormBuilder, @Inject(WINDOW) protected win: Window, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); } async ngOnInit() { @@ -184,11 +186,11 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements await this.setupCaptcha(); if (this.token == null || this.token === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("verificationCodeRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verificationCodeRequired"), + }); return; } @@ -202,11 +204,11 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements await this.handleLoginResponse(authResult); } catch { this.logService.error("Error submitting two factor token"); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidVerificationCode"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidVerificationCode"), + }); } } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 8c849db6c63..eaff9d665fd 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -97,7 +97,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected accountService: AccountService, protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); } @@ -135,7 +135,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.submit(); }, (error: string) => { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: error, + }); }, (info: string) => { if (info === "ready") { @@ -201,11 +205,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI await this.setupCaptcha(); if (this.token == null || this.token === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("verificationCodeRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verificationCodeRequired"), + }); return; } @@ -243,11 +247,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI return false; } - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("encryptionKeyMigrationRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("encryptionKeyMigrationRequired"), + }); return true; } @@ -414,11 +418,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } if ((await this.loginStrategyService.getEmail()) == null) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("sessionTimeout"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("sessionTimeout"), + }); return; } @@ -434,11 +438,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), + }); } } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 661e50e887a..98dd4aeb49a 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -19,7 +19,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -50,6 +50,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( i18nService, @@ -63,6 +64,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -77,11 +79,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise { if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } @@ -92,7 +94,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { try { await this.userVerificationService.verifyUser(secret); } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e.message, + }); return false; } @@ -120,11 +126,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.apiService.postPassword(request); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("masterPasswordChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("masterPasswordChanged"), + message: this.i18nService.t("logBackIn"), + }); if (this.onSuccessfulChangePassword != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 4991e2b152f..78fb9b625b9 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -24,7 +24,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -64,6 +64,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp kdfConfigService: KdfConfigService, accountService: AccountService, masterPasswordService: InternalMasterPasswordServiceAbstraction, + toastService: ToastService, ) { super( i18nService, @@ -77,6 +78,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -176,11 +178,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp } await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("updatedMasterPassword"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedMasterPassword"), + }); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setForceSetPasswordReason( diff --git a/libs/angular/src/auth/components/user-verification-prompt.component.ts b/libs/angular/src/auth/components/user-verification-prompt.component.ts index d999042722d..4b25a7cc0fe 100644 --- a/libs/angular/src/auth/components/user-verification-prompt.component.ts +++ b/libs/angular/src/auth/components/user-verification-prompt.component.ts @@ -5,6 +5,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { ModalRef } from "../../components/modal/modal.ref"; @@ -37,6 +38,7 @@ export class UserVerificationPromptComponent { private formBuilder: FormBuilder, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} get secret() { @@ -56,7 +58,11 @@ export class UserVerificationPromptComponent { this.invalidSecret = false; } catch (e) { this.invalidSecret = true; - this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); return; } diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts index f8746b5b24e..0c3e6ab71e8 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts @@ -15,6 +15,7 @@ import { CalloutModule, DialogModule, DialogService, + ToastService, } from "@bitwarden/components"; import { ActiveClientVerificationOption } from "./active-client-verification-option.enum"; @@ -58,6 +59,7 @@ export class UserVerificationDialogComponent { private userVerificationService: UserVerificationService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} /** @@ -256,19 +258,27 @@ export class UserVerificationDialogComponent { // Only pin should ever get here, but added this check to be safe. if (this.activeClientVerificationOption === ActiveClientVerificationOption.Pin) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("error"), - this.i18nService.t("invalidPin"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: this.i18nService.t("invalidPin"), + }); } else { - this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unexpectedError"), + }); } } } catch (e) { // Catch handles OTP and MP verification scenarios as those throw errors on verification failure instead of returning false like PIN and biometrics. this.invalidSecret = true; - this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); return; } }; From 5b4e4d8f1a19b801634beab4e801e025a34dbc62 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 27 Aug 2024 13:31:44 -0500 Subject: [PATCH 07/18] [PM-10669] Fix inconsistencies with notification bar when saving or updating login credentials (#10617) * [PM-10669] Notification bar appears inconsistently after login * [PM-10669] Notification bar appears inconsistently after login * [PM-10669] Migrating work from POC branch into feature branch * [PM-10669] Incorporating styles for select element * [PM-10669] Incorporating styles for select element * [PM-10669] Fixing notification bar lifespan const * [PM-10669] Incorporating logic that conditionally loads specific bootstrap autofill feature files * [PM-10669] Incorporating logic to more smoothly handle transitioning between pages within the notification bar0 * [PM-10669] Incorporating logic to more smoothly handle transitioning between pages within the notification bar0 * [PM-10669] Incorporating logic to more smoothly handle transitioning between pages within the notification bar0 * [PM-10669] Incorporating a circle checkmark icon within the success message of the notification bar * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing how we handle keyup events on the submit button * [PM-10669] Fixing how we handle keyup events on the submit button * [PM-10669] Fixing jest tests within notification bar * [PM-10669] Adding a jest tests to validate behavior within AutofillInit * [PM-10669] Adding a jest tests to validate behavior within AutofillInit * [PM-11170] Addressing test coverage within CollectAutofillContentService * [PM-11170] Addressing test coverage within CollectAutofillContentService * [PM-10669] Refactoring implementation * [PM-10669] Adding documentation to the methods incorporated within the AutofillOverlayContentService * [PM-10669] Incorporating jest tests for the AutofillOverlayContentService * [PM-10669] Migrating logic associated with the DomQuerySevice away from the CollectAutofillContentService * [PM-10669] Fixing required references to DomQueryService within the implementation * [PM-10669] Holding off on re-incorporating the userTreeWalkerStrategyFlag * [PM-10669] Incorporating jest tests for DomQueryService * [PM-10669] Adding jest test to validate changes within AutofillService * [PM-10669] Adding jest tests to validate changes within AutofillOverlayContentService * [PM-10669] Adding documentation to the OverlayNotificationsBackground class * [PM-10669] Adding documentation to the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Refactoring OverlayNotificationsContentService and incorporating logic that triggers a fade out of the notification bar on success of a saved password * [PM-10669] Refactoring OverlayNotificationsContentService and incorporating logic that triggers a fade out of the notification bar on success of a saved password * [PM-10669] Refactoring OverlayNotificationsContentService and incorporating logic that triggers a fade out of the notification bar on success of a saved password * [PM-10669] Finalizing jest tests for OverlayNotificationsContentService * [PM-10669] Finalizing jest tests for OverlayNotificationsContentService * [PM-10669] Adding new copy for the password saved/updated event in the notification bar * [PM-10669] Fixing visual presentation of sucesss message * [PM-10669] Fixing visual presentation of sucesss message * [PM-10418] Incorporating fallback for when we cannot capture the form button effectively * [PM-10669] Incorporating fixes for form submission button not being captured * [PM-10669] Incorporating a guard to ensure that an AJAX submission captures form data after the user has entered their credentials * [PM-10669] Incorporating a field qualification rule to ensure that we capture forms that are non-viewable on load * [PM-10669] Incorporating a document readyState listener to ensure that we populate the notification bar once the document body is loaded * [PM-10669] Incorporating a match pattern for subdomains of a main domain when filtering out web requests * [PM-10669] Incorporating a match pattern for subdomains of a main domain when filtering out web requests * [PM-10669] Incorporating a redundant methodology to capture `GET` requests that trigger after a form submisson * [PM-10669] Incorporating a redundant methodology to capture `GET` requests that trigger after a form submisson * [PM-10669] Adding jest tests to validate changes within OverlayNotificationsBackground * [PM-10669] Adjusting timeout for modified login credentials to ensure user can enter data on form * [PM-10669] Refining how we handle re-capturing user credentails on before request to better handle multi-part forms * [PM-10669] Refining how we handle re-capturing user credentails on before request to better handle multi-part forms * [PM-10669] Adjusting jest tests to ensure code coverage * [PM-10669] Fixing issues with Safari * [PM-10669] Fixing an invalid qualification rule * [PM-10669] Ensuring that we capture input changes correctly when a field is going from a hidden to non-hidden state * [PM-10669] Fixing jest tests within overlay content service * [PM-10669] Fixing jest tests within overlay content service * [PM-10669] Adding a jest test to validate changes to overlay content service --- apps/browser/src/_locales/en/messages.json | 8 + .../abstractions/notification.background.ts | 7 +- .../overlay-notifications.background.ts | 52 ++ .../notification.background.spec.ts | 74 +-- .../background/notification.background.ts | 51 +- .../overlay-notifications.background.spec.ts | 548 +++++++++++++++++ .../overlay-notifications.background.ts | 557 ++++++++++++++++++ .../content/auto-submit-login.spec.ts | 14 +- .../src/autofill/content/auto-submit-login.ts | 12 +- .../autofill/content/autofill-init.spec.ts | 23 +- .../src/autofill/content/autofill-init.ts | 22 +- .../bootstrap-autofill-overlay-menu.ts | 30 + ...ootstrap-autofill-overlay-notifications.ts | 33 ++ .../content/bootstrap-autofill-overlay.ts | 10 + .../autofill/content/bootstrap-autofill.ts | 4 +- .../src/autofill/content/notification-bar.ts | 3 +- .../content/autofill-init.deprecated.ts | 3 + ...fill-overlay-content.service.deprecated.ts | 2 +- .../autofill/enums/autofill-field.enums.ts | 1 + .../abstractions/notification-bar.ts | 2 + .../src/autofill/notification/bar.html | 27 +- .../src/autofill/notification/bar.scss | 174 +++++- apps/browser/src/autofill/notification/bar.ts | 21 +- ...tofill-inline-menu-content.service.spec.ts | 7 +- .../overlay-notifications-content.service.ts | 40 ++ ...notifications-content.service.spec.ts.snap | 14 + ...rlay-notifications-content.service.spec.ts | 264 +++++++++ .../overlay-notifications-content.service.ts | 281 +++++++++ .../autofill-overlay-content.service.ts | 10 +- .../collect-autofill-content.service.ts | 10 - .../abstractions/dom-query.service.ts | 12 + ...nline-menu-field-qualifications.service.ts | 4 + .../autofill/services/autofill-constants.ts | 18 + .../autofill-overlay-content.service.spec.ts | 401 +++++++++++-- .../autofill-overlay-content.service.ts | 282 ++++++++- .../services/autofill.service.spec.ts | 51 +- .../src/autofill/services/autofill.service.ts | 83 ++- .../collect-autofill-content.service.spec.ts | 53 +- .../collect-autofill-content.service.ts | 270 ++------- .../services/dom-query.service.spec.ts | 82 +++ .../autofill/services/dom-query.service.ts | 185 ++++++ ...e-menu-field-qualification.service.spec.ts | 15 +- ...inline-menu-field-qualification.service.ts | 77 ++- .../insert-autofill-content.service.spec.ts | 4 + .../insert-autofill-content.service.ts | 11 +- .../src/autofill/spec/testing-utils.ts | 9 + apps/browser/src/autofill/utils/index.ts | 2 +- apps/browser/src/autofill/utils/svg-icons.ts | 3 + .../browser/src/background/main.background.ts | 13 +- .../src/popup/services/services.module.ts | 1 + apps/browser/test.setup.ts | 4 + apps/browser/webpack.config.js | 4 + libs/common/src/autofill/constants/index.ts | 6 + libs/common/src/enums/feature-flag.enum.ts | 2 + 54 files changed, 3412 insertions(+), 484 deletions(-) create mode 100644 apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts create mode 100644 apps/browser/src/autofill/background/overlay-notifications.background.spec.ts create mode 100644 apps/browser/src/autofill/background/overlay-notifications.background.ts create mode 100644 apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts create mode 100644 apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts create mode 100644 apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts create mode 100644 apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap create mode 100644 apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts create mode 100644 apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts create mode 100644 apps/browser/src/autofill/services/abstractions/dom-query.service.ts create mode 100644 apps/browser/src/autofill/services/dom-query.service.spec.ts create mode 100644 apps/browser/src/autofill/services/dom-query.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index cd2cc912930..4f1696461ae 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3645,10 +3645,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index e01e2c5c02b..ed9d8e6d84b 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -9,6 +9,7 @@ interface NotificationQueueMessage { type: NotificationQueueMessageTypes; domain: string; tab: chrome.tabs.Tab; + launchTimestamp: number; expires: Date; wasVaultLocked: boolean; } @@ -88,10 +89,9 @@ type NotificationBackgroundExtensionMessage = { tab?: chrome.tabs.Tab; sender?: string; notificationType?: string; + fadeOutNotification?: boolean; }; -type SaveOrUpdateCipherResult = undefined | { error: string }; - type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage }; type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; @@ -100,7 +100,7 @@ type NotificationBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; - bgCloseNotificationBar: ({ sender }: BackgroundSenderParam) => Promise; + bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -129,7 +129,6 @@ export { ChangePasswordMessageData, UnlockVaultMessageData, AddLoginMessageData, - SaveOrUpdateCipherResult, NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, }; diff --git a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts new file mode 100644 index 00000000000..0ec6a9ae04a --- /dev/null +++ b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts @@ -0,0 +1,52 @@ +import AutofillPageDetails from "../../models/autofill-page-details"; + +export type NotificationTypeData = { + isVaultLocked?: boolean; + theme?: string; + removeIndividualVault?: boolean; + importType?: string; + launchTimestamp?: number; +}; + +export type WebsiteOriginsWithFields = Map>; + +export type ActiveFormSubmissionRequests = Set; + +export type ModifyLoginCipherFormData = { + uri: string; + username: string; + password: string; + newPassword: string; +}; + +export type ModifyLoginCipherFormDataForTab = Map< + chrome.tabs.Tab["id"], + { uri: string; username: string; password: string; newPassword: string } +>; + +export type OverlayNotificationsExtensionMessage = { + command: string; + uri?: string; + username?: string; + password?: string; + newPassword?: string; + details?: AutofillPageDetails; +}; + +type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage }; +type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender }; +type OverlayNotificationsMessageHandlersParams = OverlayNotificationsMessageParams & + OverlayNotificationSenderParams; + +export type OverlayNotificationsExtensionMessageHandlers = { + [key: string]: ({ message, sender }: OverlayNotificationsMessageHandlersParams) => any; + formFieldSubmitted: ({ message, sender }: OverlayNotificationsMessageHandlersParams) => void; + collectPageDetailsResponse: ({ + message, + sender, + }: OverlayNotificationsMessageHandlersParams) => Promise; +}; + +export interface OverlayNotificationsBackground { + init(): void; +} diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index f3ebe5b1cc9..0ede9b96091 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,4 +1,4 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; @@ -48,7 +48,8 @@ describe("NotificationBackground", () => { let notificationBackground: NotificationBackground; const autofillService = mock(); const cipherService = mock(); - const authService = mock(); + let activeAccountStatusMock$: BehaviorSubject; + let authService: MockProxy; const policyService = mock(); const folderService = mock(); const userNotificationSettingsService = mock(); @@ -60,6 +61,9 @@ describe("NotificationBackground", () => { const accountService = mock(); beforeEach(() => { + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Locked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; notificationBackground = new NotificationBackground( autofillService, cipherService, @@ -91,6 +95,7 @@ describe("NotificationBackground", () => { tab: createChromeTabMock(), expires: new Date(), wasVaultLocked: false, + launchTimestamp: 0, }; const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); @@ -126,6 +131,7 @@ describe("NotificationBackground", () => { tab: createChromeTabMock(), expires: new Date(), wasVaultLocked: false, + launchTimestamp: 0, }; const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"]( message, @@ -222,6 +228,7 @@ describe("NotificationBackground", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( sender.tab, "closeNotificationBar", + { fadeOutNotification: false }, ); }); }); @@ -249,7 +256,6 @@ describe("NotificationBackground", () => { describe("bgAddLogin message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; - let getAuthStatusSpy: jest.SpyInstance; let getEnableAddedLoginPromptSpy: jest.SpyInstance; let getEnableChangedPasswordPromptSpy: jest.SpyInstance; let pushAddLoginToQueueSpy: jest.SpyInstance; @@ -259,7 +265,6 @@ describe("NotificationBackground", () => { beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); getEnableAddedLoginPromptSpy = jest.spyOn( notificationBackground as any, "getEnableAddedLoginPrompt", @@ -281,12 +286,11 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut); + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); }); @@ -296,12 +300,11 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); }); @@ -311,13 +314,12 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -329,14 +331,13 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false); getAllDecryptedForUrlSpy.mockResolvedValueOnce([]); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -348,7 +349,7 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -358,7 +359,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled(); @@ -371,7 +371,7 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), @@ -380,7 +380,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -390,13 +389,12 @@ describe("NotificationBackground", () => { it("adds the login to the queue if the user has a locked account", async () => { const login = { username: "test", password: "password", url: "https://example.com" }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true); }); @@ -407,7 +405,7 @@ describe("NotificationBackground", () => { url: "https://example.com", } as any; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "anotherTestUsername", password: "password" } }), @@ -416,14 +414,13 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab); }); it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => { const login = { username: "tEsT", password: "password", url: "https://example.com" }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true); getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -448,14 +445,12 @@ describe("NotificationBackground", () => { describe("bgChangedPassword message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; - let getAuthStatusSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -484,12 +479,11 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( null, "example.com", @@ -508,7 +502,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), ]); @@ -516,7 +510,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -530,7 +523,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), mock({ login: { username: "test2", password: "password" } }), @@ -539,7 +532,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -553,7 +545,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ id: "cipher-id", @@ -580,7 +572,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), mock({ login: { username: "test2", password: "password" } }), @@ -589,7 +581,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -602,7 +593,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ id: "cipher-id", @@ -658,12 +649,10 @@ describe("NotificationBackground", () => { }); describe("bgSaveCipher message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; let tabSendMessageDataSpy: jest.SpyInstance; let openUnlockPopoutSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); openUnlockPopoutSpy = jest .spyOn(notificationBackground as any, "openUnlockPopout") @@ -677,12 +666,11 @@ describe("NotificationBackground", () => { edit: false, folder: "folder-id", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(tabSendMessageDataSpy).toHaveBeenCalledWith( sender.tab, "addToLockedVaultPendingNotifications", @@ -716,7 +704,7 @@ describe("NotificationBackground", () => { let cipherEncryptSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getDecryptedCipherByIdSpy = jest.spyOn( notificationBackground as any, "getDecryptedCipherById", @@ -1214,11 +1202,9 @@ describe("NotificationBackground", () => { }); describe("bgUnlockPopoutOpened message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; let pushUnlockVaultToQueueSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); pushUnlockVaultToQueueSpy = jest.spyOn( notificationBackground as any, "pushUnlockVaultToQueue", @@ -1236,7 +1222,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).not.toHaveBeenCalled(); expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled(); }); @@ -1246,12 +1231,11 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut); + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled(); }); @@ -1261,7 +1245,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); notificationBackground["notificationQueue"] = [mock()]; sendMockExtensionMessage(message, sender); @@ -1276,7 +1260,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9aac9b099a2..683e3d8f581 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -44,6 +44,7 @@ import { NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, } from "./abstractions/notification.background"; +import { NotificationTypeData } from "./abstractions/overlay-notifications.background"; import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background"; export default class NotificationBackground { @@ -58,7 +59,8 @@ export default class NotificationBackground { private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), bgGetFolderData: () => this.getFolderData(), - bgCloseNotificationBar: ({ sender }) => this.handleCloseNotificationBarMessage(sender), + bgCloseNotificationBar: ({ message, sender }) => + this.handleCloseNotificationBarMessage(message, sender), bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), bgAddLogin: ({ message, sender }) => this.addLogin(message, sender), @@ -132,6 +134,10 @@ export default class NotificationBackground { return await firstValueFrom(this.configService.serverConfig$); } + private async getAuthStatus() { + return await firstValueFrom(this.authService.activeAccountStatus$); + } + /** * Checks the notification queue for any messages that need to be sent to the * specified tab. If no tab is specified, the current tab will be used. @@ -186,9 +192,10 @@ export default class NotificationBackground { ) { const notificationType = notificationQueueMessage.type; - const typeData: Record = { + const typeData: NotificationTypeData = { isVaultLocked: notificationQueueMessage.wasVaultLocked, theme: await firstValueFrom(this.themeStateService.selectedTheme$), + launchTimestamp: notificationQueueMessage.launchTimestamp, }; switch (notificationType) { @@ -230,11 +237,11 @@ export default class NotificationBackground { * @param message - The message to add to the queue * @param sender - The contextual sender of the message */ - private async addLogin( + async addLogin( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - const authStatus = await this.authService.getAuthStatus(); + const authStatus = await this.getAuthStatus(); if (authStatus === AuthenticationStatus.LoggedOut) { return; } @@ -289,6 +296,7 @@ export default class NotificationBackground { ) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddLoginQueueMessage = { type: NotificationQueueMessageType.AddLogin, username: loginInfo.username, @@ -296,7 +304,8 @@ export default class NotificationBackground { domain: loginDomain, uri: loginInfo.url, tab: tab, - expires: new Date(new Date().getTime() + NOTIFICATION_BAR_LIFESPAN_MS), + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), wasVaultLocked: isVaultLocked, }; this.notificationQueue.push(message); @@ -310,7 +319,7 @@ export default class NotificationBackground { * @param message - The message to add to the queue * @param sender - The contextual sender of the message */ - private async changedPassword( + async changedPassword( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { @@ -320,7 +329,7 @@ export default class NotificationBackground { return; } - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { await this.pushChangePasswordToQueue( null, loginDomain, @@ -380,7 +389,7 @@ export default class NotificationBackground { return; } - const currentAuthStatus = await this.authService.getAuthStatus(); + const currentAuthStatus = await this.getAuthStatus(); if (currentAuthStatus !== AuthenticationStatus.Locked || this.notificationQueue.length) { return; } @@ -399,7 +408,7 @@ export default class NotificationBackground { * @param importType - The type of import that is being requested */ async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.authService.getAuthStatus(); + const currentAuthStatus = await this.getAuthStatus(); if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { return; } @@ -419,13 +428,15 @@ export default class NotificationBackground { ) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddChangePasswordQueueMessage = { type: NotificationQueueMessageType.ChangePassword, cipherId: cipherId, newPassword: newPassword, domain: loginDomain, tab: tab, - expires: new Date(new Date().getTime() + NOTIFICATION_BAR_LIFESPAN_MS), + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), wasVaultLocked: isVaultLocked, }; this.notificationQueue.push(message); @@ -434,11 +445,13 @@ export default class NotificationBackground { private async pushUnlockVaultToQueue(loginDomain: string, tab: chrome.tabs.Tab) { this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddUnlockVaultQueueMessage = { type: NotificationQueueMessageType.UnlockVault, domain: loginDomain, tab: tab, - expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds + launchTimestamp, + expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds wasVaultLocked: true, }; await this.sendNotificationQueueMessage(tab, message); @@ -459,11 +472,13 @@ export default class NotificationBackground { importType?: string, ) { this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddRequestFilelessImportQueueMessage = { type: NotificationQueueMessageType.RequestFilelessImport, domain: loginDomain, tab, - expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds + launchTimestamp, + expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds wasVaultLocked: false, importType, }; @@ -484,7 +499,7 @@ export default class NotificationBackground { message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { await BrowserApi.tabSendMessageData(sender.tab, "addToLockedVaultPendingNotifications", { commandToRetry: { message: { @@ -736,10 +751,16 @@ export default class NotificationBackground { * Sends a message back to the sender tab which * triggers closure of the notification bar. * + * @param message - The extension message * @param sender - The contextual sender of the message */ - private async handleCloseNotificationBarMessage(sender: chrome.runtime.MessageSender) { - await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); + private async handleCloseNotificationBarMessage( + message: NotificationBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }); } /** diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts new file mode 100644 index 00000000000..d694438c00f --- /dev/null +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -0,0 +1,548 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data"; + +import { BrowserApi } from "../../platform/browser/browser-api"; +import AutofillField from "../models/autofill-field"; +import AutofillPageDetails from "../models/autofill-page-details"; +import { + flushPromises, + sendMockExtensionMessage, + triggerTabOnRemovedEvent, + triggerTabOnUpdatedEvent, + triggerWebNavigationOnCompletedEvent, + triggerWebRequestOnBeforeRequestEvent, + triggerWebRequestOnCompletedEvent, +} from "../spec/testing-utils"; + +import NotificationBackground from "./notification.background"; +import { OverlayNotificationsBackground } from "./overlay-notifications.background"; + +describe("OverlayNotificationsBackground", () => { + let logService: MockProxy; + let configService: MockProxy; + let notificationBackground: NotificationBackground; + let getEnableChangedPasswordPromptSpy: jest.SpyInstance; + let getEnableAddedLoginPromptSpy: jest.SpyInstance; + let overlayNotificationsBackground: OverlayNotificationsBackground; + + beforeEach(async () => { + jest.useFakeTimers(); + logService = mock(); + configService = mock(); + notificationBackground = mock(); + getEnableChangedPasswordPromptSpy = jest + .spyOn(notificationBackground, "getEnableChangedPasswordPrompt") + .mockResolvedValue(true); + getEnableAddedLoginPromptSpy = jest + .spyOn(notificationBackground, "getEnableAddedLoginPrompt") + .mockResolvedValue(true); + overlayNotificationsBackground = new OverlayNotificationsBackground( + logService, + configService, + notificationBackground, + ); + configService.getFeatureFlag.mockResolvedValue(true); + await overlayNotificationsBackground.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + describe("setting up the form submission listeners", () => { + let fields: MockProxy[]; + let details: MockProxy; + + beforeEach(() => { + fields = [mock(), mock(), mock()]; + details = mock({ fields }); + }); + + describe("skipping setting up the web request listeners", () => { + it("skips setting up listeners when the notification bar is disabled", async () => { + getEnableChangedPasswordPromptSpy.mockResolvedValue(false); + getEnableAddedLoginPromptSpy.mockResolvedValue(false); + + sendMockExtensionMessage({ + command: "collectPageDetailsResponse", + details, + }); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + describe("when the sender is from an excluded domain", () => { + const senderHost = "example.com"; + const senderUrl = `https://${senderHost}`; + + beforeEach(() => { + jest.spyOn(notificationBackground, "getExcludedDomains").mockResolvedValue({ + [senderHost]: null, + }); + }); + + it("skips setting up listeners when the sender is the user's vault", async () => { + const vault = "https://vault.bitwarden.com"; + const sender = mock({ origin: vault }); + jest + .spyOn(notificationBackground, "getActiveUserServerConfig") + .mockResolvedValue( + mock({ environment: mock({ vault }) }), + ); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + it("skips setting up listeners when the sender is an excluded domain", async () => { + const sender = mock({ origin: senderUrl }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + it("skips setting up listeners when the sender contains a malformed origin", async () => { + const senderOrigin = "-_-!..exampwle.com"; + const sender = mock({ origin: senderOrigin }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + }); + + it("skips setting up listeners when the sender tab does not contain page details fields", async () => { + const sender = mock({ tab: { id: 1 } }); + details.fields = []; + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + }); + + it("sets up the web request listeners", async () => { + const sender = mock({ + tab: { id: 1 }, + url: "example.com", + }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).toHaveBeenCalled(); + }); + + it("skips setting up duplicate listeners when the website origin has been previously encountered with fields", async () => { + const sender = mock({ + tab: { id: 1 }, + url: "example.com", + }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).toHaveBeenCalledTimes(1); + }); + }); + + describe("storing the modified login form data", () => { + const sender = mock({ tab: { id: 1 } }); + + it("stores the modified login cipher form data", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }); + }); + + it("clears the modified login cipher form data after 5 seconds", () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + + jest.advanceTimersByTime(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION); + + expect(overlayNotificationsBackground["modifyLoginCipherFormData"].size).toBe(0); + }); + + it("attempts to store the modified login cipher form data within the onBeforeRequest listener when the data is not captured through a submit button click event", async () => { + const pageDetails = mock({ fields: [mock()] }); + const tab = mock({ id: sender.tab.id }); + jest.spyOn(BrowserApi, "getTab").mockResolvedValueOnce(tab); + const response = { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }; + jest.spyOn(BrowserApi, "tabSendMessage").mockResolvedValueOnce(response); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: "https://example.com", + tabId: sender.tab.id, + method: "POST", + requestId: "123345", + }), + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }); + }); + }); + + describe("web request listeners", () => { + let sender: MockProxy; + const pageDetails = mock({ fields: [mock()] }); + let notificationChangedPasswordSpy: jest.SpyInstance; + let notificationAddLoginSpy: jest.SpyInstance; + + beforeEach(async () => { + sender = mock({ + tab: { id: 1 }, + url: "https://example.com", + }); + notificationChangedPasswordSpy = jest.spyOn(notificationBackground, "changedPassword"); + notificationAddLoginSpy = jest.spyOn(notificationBackground, "addLogin"); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + }); + + describe("ignored web requests", () => { + it("ignores requests from urls that do not start with a valid protocol", async () => { + sender.url = "chrome-extension://extension-id"; + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests from urls that do not have a valid tabId", async () => { + sender.tab = mock({ id: -1 }); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests from urls that do not have a valid request method", async () => { + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "GET", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests that are not part of an active form submission", async () => { + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId: "123345", + }), + ); + + expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); + expect(notificationAddLoginSpy).not.toHaveBeenCalled(); + }); + + it("ignores requests for tabs that do not contain stored login data", async () => { + const requestId = "123345"; + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + + expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); + expect(notificationAddLoginSpy).not.toHaveBeenCalled(); + }); + }); + + describe("web requests that trigger notifications", () => { + const requestId = "123345"; + + beforeEach(async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + }); + + it("waits for the tab's navigation to complete using the web navigation API before initializing the notification", async () => { + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "loading", + url: sender.url, + }), + ); + }); + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + triggerWebNavigationOnCompletedEvent( + mock({ + tabId: sender.tab.id, + url: sender.url, + }), + ); + await flushPromises(); + + expect(notificationAddLoginSpy).toHaveBeenCalled(); + }); + + it("initializes the notification immediately when the tab's navigation is complete", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + expect(notificationAddLoginSpy).toHaveBeenCalled(); + }); + + it("triggers the notification on the beforeRequest listener when a post-submission redirection is encountered", async () => { + sender.tab = mock({ id: 4 }); + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + chrome.tabs.get = jest.fn().mockImplementation((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: "https://example.com/redirect", + tabId: sender.tab.id, + method: "GET", + requestId, + }), + ); + await flushPromises(); + + expect(notificationChangedPasswordSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("tab listeners", () => { + let sender: MockProxy; + const pageDetails = mock({ fields: [mock()] }); + const requestId = "123345"; + + beforeEach(async () => { + sender = mock({ + tab: { id: 1 }, + url: "https://example.com", + }); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + }); + + it("clears all associated data with a removed tab", () => { + triggerTabOnRemovedEvent(sender.tab.id, mock()); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); + + it("clears all associated data with a tab that is entering a `loading` state", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ status: "loading" }), + mock({ status: "loading" }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); + }); +}); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts new file mode 100644 index 00000000000..e252bdcc4af --- /dev/null +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -0,0 +1,557 @@ +import { Subject, switchMap, timer } from "rxjs"; + +import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { BrowserApi } from "../../platform/browser/browser-api"; + +import { + ActiveFormSubmissionRequests, + ModifyLoginCipherFormData, + ModifyLoginCipherFormDataForTab, + OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface, + OverlayNotificationsExtensionMessage, + OverlayNotificationsExtensionMessageHandlers, + WebsiteOriginsWithFields, +} from "./abstractions/overlay-notifications.background"; +import NotificationBackground from "./notification.background"; + +export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface { + private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map(); + private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set(); + private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map(); + private clearLoginCipherFormDataSubject: Subject = new Subject(); + private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]); + private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = { + formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender), + collectPageDetailsResponse: ({ message, sender }) => + this.handleCollectPageDetailsResponse(message, sender), + }; + + constructor( + private logService: LogService, + private configService: ConfigService, + private notificationBackground: NotificationBackground, + ) {} + + /** + * Initialize the overlay notifications background service. + */ + async init() { + const featureFlagActive = await this.configService.getFeatureFlag( + FeatureFlag.NotificationBarAddLoginImprovements, + ); + if (!featureFlagActive) { + return; + } + + this.setupExtensionListeners(); + this.clearLoginCipherFormDataSubject + .pipe(switchMap(() => timer(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION))) + .subscribe(() => this.modifyLoginCipherFormData.clear()); + } + + /** + * Handles the response from the content script with the page details. Triggers an initialization + * of the add login or change password notification if the conditions are met. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private async handleCollectPageDetailsResponse( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (await this.shouldInitAddLoginOrChangePasswordNotification(message, sender)) { + this.websiteOriginsWithFields.set(sender.tab.id, this.getSenderUrlMatchPatterns(sender)); + this.setupWebRequestsListeners(); + } + } + + /** + * Determines if the add login or change password notification should be initialized. This depends + * on whether the user has enabled the notification, the sender is not from an excluded domain, the + * tab's page details contains fillable fields, and the website origin has not been previously stored. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private async shouldInitAddLoginOrChangePasswordNotification( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + return ( + (await this.isAddLoginOrChangePasswordNotificationEnabled()) && + !(await this.isSenderFromExcludedDomain(sender)) && + message.details?.fields?.length > 0 && + !this.websiteOriginsWithFields.has(sender.tab.id) + ); + } + + /** + * Determines if the add login or change password notification is enabled. + * This is based on the user's settings for the notification. + */ + private async isAddLoginOrChangePasswordNotificationEnabled() { + return ( + (await this.notificationBackground.getEnableChangedPasswordPrompt()) || + (await this.notificationBackground.getEnableAddedLoginPrompt()) + ); + } + + /** + * Returns the match patterns for the sender's URL. This is used to filter out + * the web requests that are not from the sender's tab. + * + * @param sender - The sender of the message + */ + private getSenderUrlMatchPatterns(sender: chrome.runtime.MessageSender) { + return new Set([ + ...this.generateMatchPatterns(sender.url), + ...this.generateMatchPatterns(sender.tab.url), + ]); + } + + /** + * Generates the origin and subdomain match patterns for the URL. + * + * @param url - The URL of the tab + */ + private generateMatchPatterns(url: string): string[] { + try { + if (!url.startsWith("http")) { + url = `https://${url}`; + } + + const originMatchPattern = `${new URL(url).origin}/*`; + + const parsedUrl = new URL(url); + const splitHost = parsedUrl.hostname.split("."); + const domain = splitHost.slice(-2).join("."); + const subDomainMatchPattern = `${parsedUrl.protocol}//*.${domain}/*`; + + return [originMatchPattern, subDomainMatchPattern]; + } catch { + return []; + } + } + + /** + * Stores the login form data that was modified by the user in the content script. This data is + * used to trigger the add login or change password notification when the form is submitted. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private storeModifiedLoginFormData = ( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) => { + const { uri, username, password, newPassword } = message; + if (!username && !password && !newPassword) { + return; + } + + this.clearLoginCipherFormDataSubject.next(); + const formData = { uri, username, password, newPassword }; + + const existingModifyLoginData = this.modifyLoginCipherFormData.get(sender.tab.id); + if (existingModifyLoginData) { + formData.username = formData.username || existingModifyLoginData.username; + formData.password = formData.password || existingModifyLoginData.password; + formData.newPassword = formData.newPassword || existingModifyLoginData.newPassword; + } + + this.modifyLoginCipherFormData.set(sender.tab.id, formData); + }; + + /** + * Determines if the sender of the message is from an excluded domain. This is used to prevent the + * add login or change password notification from being triggered on the user's vault domain or + * other excluded domains. + * + * @param sender - The sender of the message + */ + private async isSenderFromExcludedDomain(sender: chrome.runtime.MessageSender): Promise { + try { + const senderOrigin = sender.origin; + const serverConfig = await this.notificationBackground.getActiveUserServerConfig(); + const activeUserVault = serverConfig?.environment?.vault; + if (activeUserVault === senderOrigin) { + return true; + } + + const excludedDomains = await this.notificationBackground.getExcludedDomains(); + if (!excludedDomains) { + return false; + } + + const senderDomain = new URL(senderOrigin).hostname; + return excludedDomains[senderDomain] !== undefined; + } catch { + return true; + } + } + + /** + * Removes and resets the onBeforeRequest and onCompleted listeners for web requests. This ensures + * that we are only listening for form submission requests on the tabs that have fillable form fields. + */ + private setupWebRequestsListeners() { + chrome.webRequest.onBeforeRequest.removeListener(this.handleOnBeforeRequestEvent); + chrome.webRequest.onCompleted.removeListener(this.handleOnCompletedRequestEvent); + if (this.websiteOriginsWithFields.size) { + const requestFilter: chrome.webRequest.RequestFilter = this.generateRequestFilter(); + chrome.webRequest.onBeforeRequest.addListener(this.handleOnBeforeRequestEvent, requestFilter); + chrome.webRequest.onCompleted.addListener(this.handleOnCompletedRequestEvent, requestFilter); + } + } + + /** + * Generates the request filter for the web requests. This is used to filter out the web requests + * that are not from the tabs that have fillable form fields. + */ + private generateRequestFilter(): chrome.webRequest.RequestFilter { + const websiteOrigins = Array.from(this.websiteOriginsWithFields.values()); + const urls: string[] = []; + websiteOrigins.forEach((origins) => urls.push(...origins)); + return { + urls, + types: ["main_frame", "sub_frame", "xmlhttprequest"], + }; + } + + /** + * Handles the onBeforeRequest event for web requests. This is used to ensures that the following + * onCompleted event is only triggered for form submission requests. + * + * @param details - The details of the web request + */ + private handleOnBeforeRequestEvent = (details: chrome.webRequest.WebRequestDetails) => { + if (this.isPostSubmissionFormRedirection(details)) { + this.setupNotificationInitTrigger( + details.tabId, + details.requestId, + this.modifyLoginCipherFormData.get(details.tabId), + ).catch((error) => this.logService.error(error)); + + return; + } + + if (!this.isValidFormSubmissionRequest(details)) { + return; + } + + const { requestId, tabId, frameId } = details; + this.activeFormSubmissionRequests.add(requestId); + + if (this.notificationDataIncompleteOnBeforeRequest(tabId)) { + this.getFormFieldDataFromTab(tabId, frameId).catch((error) => this.logService.error(error)); + } + }; + + /** + * Captures the modified login form data if the tab contains incomplete data. This is used as + * a redundancy to ensure that the modified login form data is captured in cases where the form + * is split into multiple parts. + * + * @param tabId - The id of the tab + */ + private notificationDataIncompleteOnBeforeRequest = (tabId: number) => { + const modifyLoginData = this.modifyLoginCipherFormData.get(tabId); + return ( + !modifyLoginData || + !this.shouldTriggerAddLoginNotification(modifyLoginData) || + !this.shouldTriggerChangePasswordNotification(modifyLoginData) + ); + }; + + /** + * Determines whether the request is happening after a form submission. This is identified by a GET + * request that is triggered after a form submission POST request from the same request id. If + * this is the case, and the modified login form data is available, the add login or change password + * notification is triggered. + * + * @param details - The details of the web request + */ + private isPostSubmissionFormRedirection = (details: chrome.webRequest.WebRequestDetails) => { + return ( + details.method?.toUpperCase() === "GET" && + this.activeFormSubmissionRequests.has(details.requestId) && + this.modifyLoginCipherFormData.has(details.tabId) + ); + }; + + /** + * Determines if the web request is a valid form submission request. A valid web request + * is a POST, PUT, or PATCH request that is not from an invalid host. + * + * @param details - The details of the web request + */ + private isValidFormSubmissionRequest = (details: chrome.webRequest.WebRequestDetails) => { + return ( + !this.requestHostIsInvalid(details) && + this.formSubmissionRequestMethods.has(details.method?.toUpperCase()) + ); + }; + + /** + * Retrieves the form field data from the tab. This is used to get the modified login form data + * in cases where the submit button is not clicked, but the form is submitted through other means. + * + * @param tabId - The senders tab id + * @param frameId - The frame where the form is located + */ + private getFormFieldDataFromTab = async (tabId: number, frameId: number) => { + const tab = await BrowserApi.getTab(tabId); + if (!tab) { + return; + } + + const response = (await BrowserApi.tabSendMessage( + tab, + { command: "getFormFieldDataForNotification" }, + { frameId }, + )) as OverlayNotificationsExtensionMessage; + if (response) { + this.storeModifiedLoginFormData(response, { tab }); + } + }; + + /** + * Handles the onCompleted event for web requests. This is used to trigger the add login or change + * password notification when a form submission request is completed. + * + * @param details - The details of the web response + */ + private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => { + if ( + this.requestHostIsInvalid(details) || + this.isInvalidStatusCode(details.statusCode) || + !this.activeFormSubmissionRequests.has(details.requestId) + ) { + return; + } + + const modifyLoginData = this.modifyLoginCipherFormData.get(details.tabId); + if (!modifyLoginData) { + return; + } + + this.setupNotificationInitTrigger(details.tabId, details.requestId, modifyLoginData).catch( + (error) => this.logService.error(error), + ); + }; + + /** + * Sets up the initialization trigger for the add login or change password notification. This is used + * to ensure that the notification is triggered after the tab has finished loading. + * + * @param tabId - The id of the tab + * @param requestId - The request id of the web request + * @param modifyLoginData - The modified login form data + */ + private setupNotificationInitTrigger = async ( + tabId: number, + requestId: string, + modifyLoginData: ModifyLoginCipherFormData, + ) => { + const tab = await BrowserApi.getTab(tabId); + if (tab.status !== "complete") { + await this.delayNotificationInitUntilTabIsComplete(tabId, requestId, modifyLoginData); + return; + } + + await this.triggerNotificationInit(requestId, modifyLoginData, tab); + }; + + /** + * Delays the initialization of the add login or change password notification + * until the tab is complete. This is used to ensure that the notification is + * triggered after the tab has finished loading. + * + * @param tabId - The id of the tab + * @param requestId - The request id of the web request + * @param modifyLoginData - The modified login form data + */ + private delayNotificationInitUntilTabIsComplete = async ( + tabId: chrome.webRequest.ResourceRequest["tabId"], + requestId: chrome.webRequest.ResourceRequest["requestId"], + modifyLoginData: ModifyLoginCipherFormData, + ) => { + const handleWebNavigationOnCompleted = async () => { + chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted); + const tab = await BrowserApi.getTab(tabId); + await this.triggerNotificationInit(requestId, modifyLoginData, tab); + }; + chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted); + }; + + /** + * Initializes the add login or change password notification based on the modified login form data + * and the tab details. This will trigger the notification to be displayed to the user. + * + * @param requestId - The details of the web response + * @param modifyLoginData - The modified login form data + * @param tab - The tab details + */ + private triggerNotificationInit = async ( + requestId: chrome.webRequest.ResourceRequest["requestId"], + modifyLoginData: ModifyLoginCipherFormData, + tab: chrome.tabs.Tab, + ) => { + if (this.shouldTriggerChangePasswordNotification(modifyLoginData)) { + // These notifications are temporarily setup as "messages" to the notification background. + // This will be structured differently in a future refactor. + await this.notificationBackground.changedPassword( + { + command: "bgChangedPassword", + data: { + url: modifyLoginData.uri, + currentPassword: modifyLoginData.password, + newPassword: modifyLoginData.newPassword, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + return; + } + + if (this.shouldTriggerAddLoginNotification(modifyLoginData)) { + await this.notificationBackground.addLogin( + { + command: "bgAddLogin", + login: { + url: modifyLoginData.uri, + username: modifyLoginData.username, + password: modifyLoginData.password || modifyLoginData.newPassword, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + } + }; + + /** + * Determines if the change password notification should be triggered. + * + * @param modifyLoginData - The modified login form data + */ + private shouldTriggerChangePasswordNotification = ( + modifyLoginData: ModifyLoginCipherFormData, + ) => { + return modifyLoginData.newPassword && !modifyLoginData.username; + }; + + /** + * Determines if the add login notification should be triggered. + * + * @param modifyLoginData - The modified login form data + */ + private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => { + return modifyLoginData.username && (modifyLoginData.password || modifyLoginData.newPassword); + }; + + /** + * Clears the completed web request and removes the modified login form data for the tab. + * + * @param requestId - The request id of the web request + * @param tab - The tab details + */ + private clearCompletedWebRequest = ( + requestId: chrome.webRequest.ResourceRequest["requestId"], + tab: chrome.tabs.Tab, + ) => { + this.activeFormSubmissionRequests.delete(requestId); + this.modifyLoginCipherFormData.delete(tab.id); + this.websiteOriginsWithFields.delete(tab.id); + this.setupWebRequestsListeners(); + }; + + /** + * Determines if the status code of the web response is invalid. An invalid status code is + * any status code that is not in the 200-299 range. + * + * @param statusCode - The status code of the web response + */ + private isInvalidStatusCode = (statusCode: number) => { + return statusCode < 200 || statusCode >= 300; + }; + + /** + * Determines if the host of the web request is invalid. An invalid host is any host that does not + * start with "http" or a tab id that is less than 0. + * + * @param details - The details of the web request + */ + private requestHostIsInvalid = (details: chrome.webRequest.ResourceRequest) => { + return !details.url?.startsWith("http") || details.tabId < 0; + }; + + /** + * Sets up the listeners for the extension messages and the tab events. + */ + private setupExtensionListeners() { + BrowserApi.messageListener("overlay-notifications", this.handleExtensionMessage); + chrome.tabs.onRemoved.addListener(this.handleTabRemoved); + chrome.tabs.onUpdated.addListener(this.handleTabUpdated); + } + + /** + * Handles messages that are sent to the extension background. + * + * @param message - The message from the content script + * @param sender - The sender of the message + * @param sendResponse - The response to send back to the content script + */ + private handleExtensionMessage = ( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction = this.extensionMessageHandlers[message.command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + Promise.resolve(messageResponse) + .then((response) => sendResponse(response)) + .catch((error) => this.logService.error(error)); + return true; + }; + + /** + * Handles the removal of a tab. This is used to remove the modified login form data for the tab. + * + * @param tabId - The id of the tab that was removed + */ + private handleTabRemoved = (tabId: number) => { + this.modifyLoginCipherFormData.delete(tabId); + if (this.websiteOriginsWithFields.has(tabId)) { + this.websiteOriginsWithFields.delete(tabId); + this.setupWebRequestsListeners(); + } + }; + + /** + * Handles the update of a tab. This is used to remove the modified + * login form data for the tab when the tab is loading. + * + * @param tabId - The id of the tab that was updated + * @param changeInfo - The change info of the tab + */ + private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (changeInfo.status === "loading" && this.websiteOriginsWithFields.has(tabId)) { + this.websiteOriginsWithFields.delete(tabId); + } + }; +} diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts index d8a192dbcab..98caee3d363 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.spec.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -12,6 +12,16 @@ let pageDetailsMock: AutofillPageDetails; let fillScriptMock: AutofillScript; let autofillFieldElementByOpidMock: FormFieldElement; +jest.mock("../services/dom-query.service", () => { + const module = jest.requireActual("../services/dom-query.service"); + return { + DomQueryService: class extends module.DomQueryService { + deepQueryElements(element: HTMLElement, queryString: string): T[] { + return Array.from(element.querySelectorAll(queryString)) as T[]; + } + }, + }; +}); jest.mock("../services/collect-autofill-content.service", () => { const module = jest.requireActual("../services/collect-autofill-content.service"); return { @@ -20,10 +30,6 @@ jest.mock("../services/collect-autofill-content.service", () => { return pageDetailsMock; } - deepQueryElements(element: HTMLElement, queryString: string): T[] { - return Array.from(element.querySelectorAll(queryString)) as T[]; - } - getAutofillFieldElementByOpid(opid: string) { const mockedEl = autofillFieldElementByOpidMock; if (mockedEl) { diff --git a/apps/browser/src/autofill/content/auto-submit-login.ts b/apps/browser/src/autofill/content/auto-submit-login.ts index 9cc06f874e6..19ffac61bcf 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.ts @@ -4,13 +4,16 @@ import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; +import { DomQueryService } from "../services/dom-query.service"; import InsertAutofillContentService from "../services/insert-autofill-content.service"; import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from "../utils"; (function (globalContext) { + const domQueryService = new DomQueryService(); const domElementVisibilityService = new DomElementVisibilityService(); const collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, + domQueryService, ); const insertAutofillContentService = new InsertAutofillContentService( domElementVisibilityService, @@ -191,7 +194,7 @@ import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from " element: HTMLElement, lastFieldIsPasswordInput = false, ): boolean { - const genericSubmitElement = collectAutofillContentService.deepQueryElements( + const genericSubmitElement = domQueryService.deepQueryElements( element, "[type='submit']", ); @@ -200,10 +203,7 @@ import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from " return true; } - const buttons = collectAutofillContentService.deepQueryElements( - element, - "button", - ); + const buttons = domQueryService.deepQueryElements(element, "button"); for (let i = 0; i < buttons.length; i++) { if (isLoginButton(buttons[i])) { clickSubmitElement(buttons[i], lastFieldIsPasswordInput); @@ -274,7 +274,7 @@ import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from " */ function getAutofillFormElements(): HTMLFormElement[] { const formElements: HTMLFormElement[] = []; - collectAutofillContentService.queryAllTreeWalkerNodes( + domQueryService.queryAllTreeWalkerNodes( globalContext.document.documentElement, (node: Node) => { if (nodeIsFormElement(node)) { diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index e27e8ef73d0..ebfbda75b56 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -3,6 +3,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; +import { DomQueryService } from "../services/abstractions/dom-query.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { flushPromises, @@ -14,6 +16,8 @@ import { AutofillExtensionMessage } from "./abstractions/autofill-init"; import AutofillInit from "./autofill-init"; describe("AutofillInit", () => { + let domQueryService: MockProxy; + let overlayNotificationsContentService: MockProxy; let inlineMenuElements: MockProxy; let autofillOverlayContentService: MockProxy; let autofillInit: AutofillInit; @@ -27,9 +31,16 @@ describe("AutofillInit", () => { addListener: jest.fn(), }, }); + domQueryService = mock(); + overlayNotificationsContentService = mock(); inlineMenuElements = mock(); autofillOverlayContentService = mock(); - autofillInit = new AutofillInit(autofillOverlayContentService, inlineMenuElements); + autofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + overlayNotificationsContentService, + ); sendExtensionMessageSpy = jest .spyOn(autofillInit as any, "sendExtensionMessage") .mockImplementation(); @@ -171,6 +182,16 @@ describe("AutofillInit", () => { expect(inlineMenuElements.messageHandlers.messageHandler).toHaveBeenCalled(); }); + it("triggers extension message handlers from the OverlayNotificationsContentService", () => { + overlayNotificationsContentService.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect( + overlayNotificationsContentService.messageHandlers.messageHandler, + ).toHaveBeenCalled(); + }); + describe("collectPageDetails", () => { it("sends the collected page details for autofill using a background script message", async () => { const pageDetails: AutofillPageDetails = { diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index e44956e1849..c0cbac3ae67 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -2,7 +2,9 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillPageDetails from "../models/autofill-page-details"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; +import { DomQueryService } from "../services/abstractions/dom-query.service"; import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; import InsertAutofillContentService from "../services/insert-autofill-content.service"; @@ -16,8 +18,6 @@ import { class AutofillInit implements AutofillInitInterface { private readonly sendExtensionMessage = sendExtensionMessage; - private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined; - private readonly autofillInlineMenuContentService: AutofillInlineMenuContentService | undefined; private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; @@ -32,20 +32,23 @@ class AutofillInit implements AutofillInitInterface { * AutofillInit constructor. Initializes the DomElementVisibilityService, * CollectAutofillContentService and InsertAutofillContentService classes. * + * @param domQueryService - Service used to handle DOM queries. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. - * @param inlineMenuElements - The inline menu elements, potentially undefined. + * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined. + * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined. */ constructor( - autofillOverlayContentService?: AutofillOverlayContentService, - inlineMenuElements?: AutofillInlineMenuContentService, + private domQueryService: DomQueryService, + private autofillOverlayContentService?: AutofillOverlayContentService, + private autofillInlineMenuContentService?: AutofillInlineMenuContentService, + private overlayNotificationsContentService?: OverlayNotificationsContentService, ) { - this.autofillOverlayContentService = autofillOverlayContentService; - this.autofillInlineMenuContentService = inlineMenuElements; this.domElementVisibilityService = new DomElementVisibilityService( this.autofillInlineMenuContentService, ); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, + domQueryService, this.autofillOverlayContentService, ); this.insertAutofillContentService = new InsertAutofillContentService( @@ -204,6 +207,10 @@ class AutofillInit implements AutofillInitInterface { return this.autofillInlineMenuContentService.messageHandlers[command]; } + if (this.overlayNotificationsContentService?.messageHandlers?.[command]) { + return this.overlayNotificationsContentService.messageHandlers[command]; + } + return this.extensionMessageHandlers[command]; } @@ -217,6 +224,7 @@ class AutofillInit implements AutofillInitInterface { this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); this.autofillInlineMenuContentService?.destroy(); + this.overlayNotificationsContentService?.destroy(); } } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts new file mode 100644 index 00000000000..aed0f6cb940 --- /dev/null +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts @@ -0,0 +1,30 @@ +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; +import { setupAutofillInitDisconnectAction } from "../utils"; + +import AutofillInit from "./autofill-init"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + let inlineMenuElements: AutofillInlineMenuContentService; + if (globalThis.self === globalThis.top) { + inlineMenuElements = new AutofillInlineMenuContentService(); + } + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + ); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts new file mode 100644 index 00000000000..0a810c68f56 --- /dev/null +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts @@ -0,0 +1,33 @@ +import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; +import { setupAutofillInitDisconnectAction } from "../utils"; + +import AutofillInit from "./autofill-init"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + + let overlayNotificationsContentService: OverlayNotificationsContentService; + if (globalThis.self === globalThis.top) { + overlayNotificationsContentService = new OverlayNotificationsContentService(); + } + + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + null, + overlayNotificationsContentService, + ); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index 22430227660..6df9397f6d8 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,5 +1,7 @@ import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; @@ -7,17 +9,25 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, inlineMenuFieldQualificationService, ); + let inlineMenuElements: AutofillInlineMenuContentService; + let overlayNotificationsContentService: OverlayNotificationsContentService; if (globalThis.self === globalThis.top) { inlineMenuElements = new AutofillInlineMenuContentService(); + overlayNotificationsContentService = new OverlayNotificationsContentService(); } + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, autofillOverlayContentService, inlineMenuElements, + overlayNotificationsContentService, ); setupAutofillInitDisconnectAction(windowContext); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill.ts b/apps/browser/src/autofill/content/bootstrap-autofill.ts index f98d4bc1d72..3de750cd671 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill.ts @@ -1,10 +1,12 @@ +import { DomQueryService } from "../services/dom-query.service"; import { setupAutofillInitDisconnectAction } from "../utils"; import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - windowContext.bitwardenAutofillInit = new AutofillInit(); + const domQueryService = new DomQueryService(); + windowContext.bitwardenAutofillInit = new AutofillInit(domQueryService); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 2bcf4394fd9..5217ebbe8ed 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -6,6 +6,7 @@ import { import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; +import { NotificationTypeData } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { FormData } from "../services/abstractions/autofill.service"; import { sendExtensionMessage, setupExtensionDisconnectAction } from "../utils"; @@ -832,7 +833,7 @@ async function loadNotificationBar() { // End Form Detection and Submission Handling // Notification Bar Functions (open, close, height adjustment, etc.) - function closeExistingAndOpenBar(type: string, typeData: any) { + function closeExistingAndOpenBar(type: string, typeData: NotificationTypeData) { const notificationBarInitData: NotificationBarIframeInitData = { type, isVaultLocked: typeData.isVaultLocked, diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts index 211e3bf9251..b3ee2637b09 100644 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts @@ -2,6 +2,7 @@ import { AutofillInit } from "../../content/abstractions/autofill-init"; import AutofillPageDetails from "../../models/autofill-page-details"; import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; import DomElementVisibilityService from "../../services/dom-element-visibility.service"; +import { DomQueryService } from "../../services/dom-query.service"; import InsertAutofillContentService from "../../services/insert-autofill-content.service"; import { sendExtensionMessage } from "../../utils"; import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; @@ -40,8 +41,10 @@ class LegacyAutofillInit implements AutofillInit { constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { this.autofillOverlayContentService = autofillOverlayContentService; this.domElementVisibilityService = new DomElementVisibilityService(); + const domQueryService = new DomQueryService(); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, + domQueryService, this.autofillOverlayContentService, ); this.insertAutofillContentService = new InsertAutofillContentService( diff --git a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts index 6526f6993db..87af2518ddc 100644 --- a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts @@ -73,7 +73,7 @@ class LegacyAutofillOverlayContentService implements LegacyAutofillOverlayConten * Satisfy the AutofillOverlayContentService interface. */ messageHandlers = {} as AutofillOverlayContentExtensionMessageHandlers; - async setupInlineMenu( + async setupOverlayListeners( autofillFieldElement: ElementWithOpId, autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, diff --git a/apps/browser/src/autofill/enums/autofill-field.enums.ts b/apps/browser/src/autofill/enums/autofill-field.enums.ts index 4fd7c0fe88f..68408f2b671 100644 --- a/apps/browser/src/autofill/enums/autofill-field.enums.ts +++ b/apps/browser/src/autofill/enums/autofill-field.enums.ts @@ -1,5 +1,6 @@ export const AutofillFieldQualifier = { password: "password", + newPassword: "newPassword", username: "username", cardholderName: "cardholderName", cardNumber: "cardNumber", diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 268617c419e..6dfcac4abea 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -4,6 +4,8 @@ type NotificationBarIframeInitData = { theme?: string; removeIndividualVault?: boolean; importType?: string; + applyRedesign?: boolean; + launchTimestamp?: number; }; type NotificationBarWindowMessage = { diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 26d9d7086d4..6b0e76b5169 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -13,16 +13,11 @@
-
+
@@ -32,8 +27,8 @@