diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 36e3ce65a8..8c81088fc5 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3000,6 +3000,9 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 8cf1a8d3e4..24aa45d5c3 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,13 +1,10 @@ -import { SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; -import { ToastrService } from "ngx-toastr"; +import { ToastService } from "@bitwarden/components"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( - private sanitizer: DomSanitizer, - private toastrService: ToastrService, + private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise, win: Window & typeof globalThis, @@ -21,20 +18,6 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService text: string | string[], options?: any, ): void { - if (typeof text === "string") { - // Already in the correct format - } else if (text.length === 1) { - text = text[0]; - } else { - let message = ""; - text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - text = message; - options.enableHtml = true; - } - this.toastrService.show(text, title, options, "toast-" + type); - // noop + this.toastService._showToast({ type, title, text, options }); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index c224e652f6..2aba93ac95 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,19 +1,18 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; -import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -35,7 +34,6 @@ export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( - private toastrService: ToastrService, private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, @@ -46,9 +44,10 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private platformUtilsService: ForegroundPlatformUtilsService, + private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -83,10 +82,10 @@ export class AppComponent implements OnInit, OnDestroy { if (msg.command === "doneLoggingOut") { this.authService.logOut(async () => { if (msg.expired) { - this.showToast({ - type: "warning", + this.toastService.showToast({ + variant: "warning", title: this.i18nService.t("loggedOut"), - text: this.i18nService.t("loginExpired"), + message: this.i18nService.t("loginExpired"), }); } @@ -116,7 +115,7 @@ export class AppComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showNativeMessagingFingerprintDialog(msg); } else if (msg.command === "showToast") { - this.showToast(msg); + this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { const forceWindowReload = this.platformUtilsService.isSafari() || diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d179868448..5718542b01 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -11,11 +11,10 @@ import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; -import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; @@ -87,7 +86,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, closeButton: true, diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss deleted file mode 100644 index e1e386d62d..0000000000 --- a/apps/browser/src/popup/scss/plugins.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; -@import "buttons.scss"; - -// Toaster - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 0d7e428138..850ef96c64 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -8,7 +8,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "pages.scss"; @import "@angular/cdk/overlay-prebuilt.css"; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 4906198047..f3be8490c1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,7 +1,5 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -83,7 +81,7 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; @@ -259,15 +257,9 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, - }), - safeProvider({ - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + useFactory: (toastService: ToastService) => { return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, + toastService, (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, @@ -284,7 +276,7 @@ const safeProviders: SafeProvider[] = [ window, ); }, - deps: [DomSanitizer, ToastrService], + deps: [ToastService], }), safeProvider({ provide: PasswordGenerationServiceAbstraction, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b2b44e6b21..ad99a3a447 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -3,14 +3,11 @@ import { NgZone, OnDestroy, OnInit, - SecurityContext, Type, ViewChild, ViewContainerRef, } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -49,7 +46,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -129,9 +126,8 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, - private sanitizer: DomSanitizer, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -294,7 +290,7 @@ export class AppComponent implements OnInit, OnDestroy { ); break; case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "copiedToClipboard": if (!message.clearing) { @@ -674,34 +670,6 @@ export class AppComponent implements OnInit, OnDestroy { }); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private routeToVault(action: string, cipherType: CipherType) { if (!this.router.url.includes("vault")) { // 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/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3d2b40ac62..ff9cbc97cc 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss deleted file mode 100644 index c156456809..0000000000 --- a/apps/desktop/src/scss/plugins.scss +++ /dev/null @@ -1,95 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 033a0f8b67..54c1385dcf 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -11,7 +11,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "header.scss"; @import "left-nav.scss"; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 7a3b34969a..1da2d94c15 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,9 +1,7 @@ import { DOCUMENT } from "@angular/common"; -import { Component, Inject, NgZone, OnDestroy, OnInit, SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; +import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { Subject, switchMap, takeUntil, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -29,7 +27,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PolicyListService } from "./admin-console/core/policy-list.service"; import { @@ -68,14 +66,13 @@ export class AppComponent implements OnDestroy, OnInit { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private cryptoService: CryptoService, private collectionService: CollectionService, - private sanitizer: DomSanitizer, private searchService: SearchService, private notificationsService: NotificationsService, private stateService: StateService, @@ -209,7 +206,7 @@ export class AppComponent implements OnDestroy, OnInit { break; } case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "convertAccountToKeyConnector": // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -327,34 +324,6 @@ export class AppComponent implements OnDestroy, OnInit { }, IdleTimeout); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private idleStateChanged() { if (this.isIdle) { // 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/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index bc775f07e2..1b04583a39 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; import { InfiniteScrollModule } from "ngx-infinite-scroll"; -import { ToastrModule } from "ngx-toastr"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -52,7 +51,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library modules @@ -90,7 +88,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 7632392c23..f14574508c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, diff --git a/apps/web/src/scss/styles.scss b/apps/web/src/scss/styles.scss index 98b3512ba5..8fbea200a9 100644 --- a/apps/web/src/scss/styles.scss +++ b/apps/web/src/scss/styles.scss @@ -43,8 +43,6 @@ @import "~bootstrap/scss/_utilities"; @import "~bootstrap/scss/_print"; -@import "~ngx-toastr/toastr"; - @import "./base"; @import "./buttons"; @import "./callouts"; @@ -54,5 +52,4 @@ @import "./pages"; @import "./plugins"; @import "./tables"; -@import "./toasts"; @import "./vault-filters"; diff --git a/apps/web/src/scss/toasts.scss b/apps/web/src/scss/toasts.scss deleted file mode 100644 index 6685de6449..0000000000 --- a/apps/web/src/scss/toasts.scss +++ /dev/null @@ -1,117 +0,0 @@ -.toast-container { - .toast-close-button { - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("danger"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textDangerColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warning"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textWarningColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("info"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textInfoColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("success"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textSuccessColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/libs/angular/src/components/toastr.component.ts b/libs/angular/src/components/toastr.component.ts deleted file mode 100644 index bfe20ed866..0000000000 --- a/libs/angular/src/components/toastr.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { CommonModule } from "@angular/common"; -import { Component, ModuleWithProviders, NgModule } from "@angular/core"; -import { - DefaultNoComponentGlobalConfig, - GlobalConfig, - Toast as BaseToast, - ToastPackage, - ToastrService, - TOAST_CONFIG, -} from "ngx-toastr"; - -@Component({ - selector: "[toast-component2]", - template: ` - -
- -
-
-
- {{ title }} [{{ duplicatesCount + 1 }}] -
-
-
- {{ message }} -
-
-
-
-
- `, - animations: [ - trigger("flyInOut", [ - state("inactive", style({ opacity: 0 })), - state("active", style({ opacity: 1 })), - state("removed", style({ opacity: 0 })), - transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), - transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), - ]), - ], - preserveWhitespaces: false, -}) -export class BitwardenToast extends BaseToast { - constructor( - protected toastrService: ToastrService, - public toastPackage: ToastPackage, - ) { - super(toastrService, toastPackage); - } -} - -export const BitwardenToastGlobalConfig: GlobalConfig = { - ...DefaultNoComponentGlobalConfig, - toastComponent: BitwardenToast, -}; - -@NgModule({ - imports: [CommonModule], - declarations: [BitwardenToast], - exports: [BitwardenToast], -}) -export class BitwardenToastModule { - static forRoot(config: Partial = {}): ModuleWithProviders { - return { - ngModule: BitwardenToastModule, - providers: [ - { - provide: TOAST_CONFIG, - useValue: { - default: BitwardenToastGlobalConfig, - config: config, - }, - }, - ], - }; - } -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 64fb44e3b8..5f1bf796aa 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,10 +2,9 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective } from "@bitwarden/components"; +import { AutofocusDirective, ToastModule } from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; -import { BitwardenToastModule } from "./components/toastr.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -34,7 +33,7 @@ import { IconComponent } from "./vault/components/icon.component"; @NgModule({ imports: [ - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 5, autoDismiss: true, closeButton: true, @@ -77,7 +76,7 @@ import { IconComponent } from "./vault/components/icon.component"; A11yTitleDirective, ApiActionDirective, AutofocusDirective, - BitwardenToastModule, + ToastModule, BoxRowDirective, CalloutComponent, CopyTextDirective, diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index d518a17f7b..f2dff46c78 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -28,6 +28,11 @@ export abstract class PlatformUtilsService { abstract getApplicationVersionNumber(): Promise; abstract supportsWebAuthn(win: Window): boolean; abstract supportsDuo(): boolean; + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + * + * Jira: [CL-213](https://bitwarden.atlassian.net/browse/CL-213) + */ abstract showToast( type: "error" | "success" | "warning" | "info", title: string, diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 139e69ebb6..527d5f3615 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -29,6 +29,7 @@ export * from "./section"; export * from "./select"; export * from "./table"; export * from "./tabs"; +export * from "./toast"; export * from "./toggle-group"; export * from "./typography"; export * from "./utils/i18n-mock.service"; diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts new file mode 100644 index 0000000000..f0b5540219 --- /dev/null +++ b/libs/components/src/toast/index.ts @@ -0,0 +1,2 @@ +export * from "./toast.module"; +export * from "./toast.service"; diff --git a/libs/components/src/toast/toast.component.html b/libs/components/src/toast/toast.component.html new file mode 100644 index 0000000000..f301995d0a --- /dev/null +++ b/libs/components/src/toast/toast.component.html @@ -0,0 +1,24 @@ +
+
+ +
+ {{ variant | i18n }} +

{{ title }}

+

+ {{ m }} +

+
+ +
+
+
diff --git a/libs/components/src/toast/toast.component.ts b/libs/components/src/toast/toast.component.ts new file mode 100644 index 0000000000..4a31e00586 --- /dev/null +++ b/libs/components/src/toast/toast.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared"; + +export type ToastVariant = "success" | "error" | "info" | "warning"; + +const variants: Record = { + success: { + icon: "bwi-check", + bgColor: "tw-bg-success-600", + }, + error: { + icon: "bwi-error", + bgColor: "tw-bg-danger-600", + }, + info: { + icon: "bwi-info-circle", + bgColor: "tw-bg-info-600", + }, + warning: { + icon: "bwi-exclamation-triangle", + bgColor: "tw-bg-warning-600", + }, +}; + +@Component({ + selector: "bit-toast", + templateUrl: "toast.component.html", + standalone: true, + imports: [SharedModule, IconButtonModule], +}) +export class ToastComponent { + @Input() variant: ToastVariant = "info"; + + /** + * The message to display + * + * Pass an array to render multiple paragraphs. + **/ + @Input({ required: true }) + message: string | string[]; + + /** An optional title to display over the message. */ + @Input() title: string; + + /** + * The percent width of the progress bar, from 0-100 + **/ + @Input() progressWidth = 0; + + /** Emits when the user presses the close button */ + @Output() onClose = new EventEmitter(); + + protected get iconClass(): string { + return variants[this.variant].icon; + } + + protected get bgColor(): string { + return variants[this.variant].bgColor; + } + + protected get messageArray(): string[] { + return Array.isArray(this.message) ? this.message : [this.message]; + } +} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts new file mode 100644 index 0000000000..bf39a0be9a --- /dev/null +++ b/libs/components/src/toast/toast.module.ts @@ -0,0 +1,39 @@ +import { CommonModule } from "@angular/common"; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrComponent } from "./toastr.component"; + +@NgModule({ + imports: [CommonModule, ToastComponent], + declarations: [BitwardenToastrComponent], + exports: [BitwardenToastrComponent], +}) +export class ToastModule { + static forRoot(config: Partial = {}): ModuleWithProviders { + return { + ngModule: ToastModule, + providers: [ + { + provide: TOAST_CONFIG, + useValue: { + default: BitwardenToastrGlobalConfig, + config: config, + }, + }, + ], + }; + } +} + +export const BitwardenToastrGlobalConfig: GlobalConfig = { + ...DefaultNoComponentGlobalConfig, + toastComponent: BitwardenToastrComponent, + tapToDismiss: false, + timeOut: 5000, + extendedTimeOut: 2000, + maxOpened: 5, + autoDismiss: true, + progressBar: true, +}; diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts new file mode 100644 index 0000000000..8bbff02c41 --- /dev/null +++ b/libs/components/src/toast/toast.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; + +import type { ToastComponent } from "./toast.component"; +import { calculateToastTimeout } from "./utils"; + +export type ToastOptions = { + /** + * The duration the toast will persist in milliseconds + **/ + timeout?: number; +} & Pick; + +/** + * Presents toast notifications + **/ +@Injectable({ providedIn: "root" }) +export class ToastService { + constructor(private toastrService: ToastrService) {} + + showToast(options: ToastOptions) { + const toastrConfig: Partial = { + payload: { + message: options.message, + variant: options.variant, + title: options.title, + }, + timeOut: + options.timeout != null && options.timeout > 0 + ? options.timeout + : calculateToastTimeout(options.message), + }; + + this.toastrService.show(null, options.title, toastrConfig); + } + + /** + * @deprecated use `showToast` instead + * + * Converts options object from PlatformUtilsService + **/ + _showToast(options: { + type: "error" | "success" | "warning" | "info"; + title: string; + text: string | string[]; + options?: { + timeout?: number; + }; + }) { + this.showToast({ + message: options.text, + variant: options.type, + title: options.title, + timeout: options.options?.timeout, + }); + } +} diff --git a/libs/components/src/toast/toast.spec.ts b/libs/components/src/toast/toast.spec.ts new file mode 100644 index 0000000000..92d8071dc5 --- /dev/null +++ b/libs/components/src/toast/toast.spec.ts @@ -0,0 +1,16 @@ +import { calculateToastTimeout } from "./utils"; + +describe("Toast default timer", () => { + it("should have a minimum of 5000ms", () => { + expect(calculateToastTimeout("")).toBe(5000); + expect(calculateToastTimeout([""])).toBe(5000); + expect(calculateToastTimeout(" ")).toBe(5000); + }); + + it("should return an extra second for each 120 words", () => { + expect(calculateToastTimeout("foo ".repeat(119))).toBe(5000); + expect(calculateToastTimeout("foo ".repeat(120))).toBe(6000); + expect(calculateToastTimeout("foo ".repeat(240))).toBe(7000); + expect(calculateToastTimeout(["foo ".repeat(120), " \n foo ".repeat(120)])).toBe(7000); + }); +}); diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts new file mode 100644 index 0000000000..d209453d85 --- /dev/null +++ b/libs/components/src/toast/toast.stories.ts @@ -0,0 +1,124 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { I18nMockService } from "../utils/i18n-mock.service"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrGlobalConfig, ToastModule } from "./toast.module"; +import { ToastOptions, ToastService } from "./toast.service"; + +const toastServiceExampleTemplate = ` + +`; +@Component({ + selector: "toast-service-example", + template: toastServiceExampleTemplate, +}) +export class ToastServiceExampleComponent { + @Input() + toastOptions: ToastOptions; + + constructor(protected toastService: ToastService) {} +} + +export default { + title: "Component Library/Toast", + component: ToastComponent, + + decorators: [ + moduleMetadata({ + imports: [CommonModule, BrowserAnimationsModule, ButtonModule], + declarations: [ToastServiceExampleComponent], + }), + applicationConfig({ + providers: [ + ToastModule.forRoot().providers, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + success: "Success", + error: "Error", + warning: "Warning", + }); + }, + }, + ], + }), + ], + args: { + onClose: action("emit onClose"), + variant: "info", + progressWidth: 50, + title: "", + message: "Hello Bitwarden!", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: ` +
+ + + + +
+ `, + }), +}; + +/** + * Avoid using long messages in toasts. + */ +export const LongContent: Story = { + ...Default, + args: { + title: "Foo", + message: [ + "Lorem ipsum dolor sit amet, consectetur adipisci", + "Lorem ipsum dolor sit amet, consectetur adipisci", + ], + }, +}; + +export const Service: Story = { + render: (args) => ({ + props: { + toastOptions: args, + }, + template: ` + + `, + }), + args: { + title: "", + message: "Hello Bitwarden!", + variant: "error", + timeout: BitwardenToastrGlobalConfig.timeOut, + } as ToastOptions, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + source: { + code: toastServiceExampleTemplate, + }, + }, + }, +}; diff --git a/libs/components/src/toast/toast.tokens.css b/libs/components/src/toast/toast.tokens.css new file mode 100644 index 0000000000..2ff9e99ae5 --- /dev/null +++ b/libs/components/src/toast/toast.tokens.css @@ -0,0 +1,4 @@ +:root { + --bit-toast-width: 19rem; + --bit-toast-width-full: 96%; +} diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts new file mode 100644 index 0000000000..70085dfc47 --- /dev/null +++ b/libs/components/src/toast/toastr.component.ts @@ -0,0 +1,26 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { Component } from "@angular/core"; +import { Toast as BaseToastrComponent } from "ngx-toastr"; + +@Component({ + template: ` + + `, + animations: [ + trigger("flyInOut", [ + state("inactive", style({ opacity: 0 })), + state("active", style({ opacity: 1 })), + state("removed", style({ opacity: 0 })), + transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), + transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), + ]), + ], + preserveWhitespaces: false, +}) +export class BitwardenToastrComponent extends BaseToastrComponent {} diff --git a/libs/components/src/toast/toastr.css b/libs/components/src/toast/toastr.css new file mode 100644 index 0000000000..fabf8caf10 --- /dev/null +++ b/libs/components/src/toast/toastr.css @@ -0,0 +1,23 @@ +@import "~ngx-toastr/toastr"; +@import "./toast.tokens.css"; + +/* Override all default styles from `ngx-toaster` */ +.toast-container .ngx-toastr { + all: unset; + display: block; + width: var(--bit-toast-width); + + /* Needed to make hover states work in Electron, since the toast appears in the draggable region. */ + -webkit-app-region: no-drag; +} + +/* Disable hover styles */ +.toast-container .ngx-toastr:hover { + box-shadow: none; +} + +.toast-container.toast-bottom-full-width .ngx-toastr { + width: var(--bit-toast-width-full); + margin-left: auto; + margin-right: auto; +} diff --git a/libs/components/src/toast/utils.ts b/libs/components/src/toast/utils.ts new file mode 100644 index 0000000000..4c8323f396 --- /dev/null +++ b/libs/components/src/toast/utils.ts @@ -0,0 +1,14 @@ +/** + * Given a toast message, calculate the ideal timeout length following: + * a minimum of 5 seconds + 1 extra second per 120 additional words + * + * @param message the toast message to be displayed + * @returns the timeout length in milliseconds + */ +export const calculateToastTimeout = (message: string | string[]): number => { + const paragraphs = Array.isArray(message) ? message : [message]; + const numWords = paragraphs + .map((paragraph) => paragraph.split(/\s+/).filter((word) => word !== "")) + .flat().length; + return 5000 + Math.floor(numWords / 120) * 1000; +}; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 0087af28ae..72e8e1e5e8 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -171,6 +171,9 @@ @import "./popover/popover.component.css"; @import "./search/search.component.css"; +@import "./toast/toast.tokens.css"; +@import "./toast/toastr.css"; + /** * tw-break-words does not work with table cells: * https://github.com/tailwindlabs/tailwindcss/issues/835