diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index bc2448d924e..89b605ab632 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4296,5 +4296,26 @@ }, "additionalContentAvailable": { "message": "Additional content is available" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 7a5b156a506..350b4a8a84d 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; -import { Subject, filter, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; +import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; import { AnonLayoutComponent, @@ -9,7 +9,6 @@ import { AnonLayoutWrapperDataService, } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { Icon, IconModule } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -17,10 +16,7 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { CurrentAccountComponent } from "../account-switching/current-account.component"; -import { - ExtensionBitwardenLogoPrimary, - ExtensionBitwardenLogoWhite, -} from "./extension-bitwarden-logo.icon"; +import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { showAcctSwitcher?: boolean; @@ -56,14 +52,13 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected maxWidth: "md" | "3xl"; protected theme: string; - protected logo: Icon; + protected logo = ExtensionBitwardenLogo; constructor( private router: Router, private route: ActivatedRoute, private i18nService: I18nService, private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService, - private themeStateService: ThemeStateService, ) {} async ngOnInit(): Promise { @@ -73,14 +68,6 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { // Listen for page changes and update the page data appropriately this.listenForPageDataChanges(); this.listenForServiceDataChanges(); - - this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); - - if (this.theme === "dark") { - this.logo = ExtensionBitwardenLogoWhite; - } else { - this.logo = ExtensionBitwardenLogoPrimary; - } } private listenForPageDataChanges() { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index 44060f991ff..beb07f3523a 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -22,8 +22,6 @@ 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 { ThemeType } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, I18nMockService } from "@bitwarden/components"; @@ -47,7 +45,6 @@ const decorators = (options: { applicationVersion?: string; clientType?: ClientType; hostName?: string; - themeType?: ThemeType; }) => { return [ componentWrapperDecorator( @@ -120,12 +117,6 @@ const decorators = (options: { getClientType: () => options.clientType || ClientType.Web, } as Partial, }, - { - provide: ThemeStateService, - useValue: { - selectedTheme$: of(options.themeType || ThemeType.Light), - } as Partial, - }, { provide: I18nService, useFactory: () => { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts index 569edaae978..51d748e1fbb 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const ExtensionBitwardenLogoPrimary = svgIcon` +export const ExtensionBitwardenLogo = svgIcon` - -`; - -export const ExtensionBitwardenLogoWhite = svgIcon` - - `; diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 505931ad0f1..cd9dfc3702b 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -41,7 +41,7 @@ export class HomeComponent implements OnInit, OnDestroy { ) {} async ngOnInit(): Promise { - const email = this.loginEmailService.getEmail(); + const email = await firstValueFrom(this.loginEmailService.loginEmail$); const rememberEmail = this.loginEmailService.getRememberEmail(); if (email != null) { @@ -93,7 +93,7 @@ export class HomeComponent implements OnInit, OnDestroy { async setLoginEmailValues() { // Note: Browser saves email settings here instead of the login component this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); - this.loginEmailService.setEmail(this.formGroup.value.email); + await this.loginEmailService.setLoginEmail(this.formGroup.value.email); await this.loginEmailService.saveEmailSettings(); } } diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 6e73199969a..09bfdbbc240 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -1,4 +1,4 @@ -import { Component, NgZone } from "@angular/core"; +import { Component, NgZone, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -31,7 +31,7 @@ import { flagEnabled } from "../../platform/flags"; selector: "app-login", templateUrl: "login.component.html", }) -export class LoginComponent extends BaseLoginComponent { +export class LoginComponent extends BaseLoginComponent implements OnInit { showPasswordless = false; constructor( devicesApiService: DevicesApiServiceAbstraction, @@ -83,13 +83,14 @@ export class LoginComponent extends BaseLoginComponent { }; super.successRoute = "/tabs/vault"; this.showPasswordless = flagEnabled("showPasswordless"); + } + async ngOnInit(): Promise { if (this.showPasswordless) { - this.formGroup.controls.email.setValue(this.loginEmailService.getEmail()); + const loginEmail = await firstValueFrom(this.loginEmailService.loginEmail$); + this.formGroup.controls.email.setValue(loginEmail); this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail()); - // 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.validateEmail(); + await this.validateEmail(); } } diff --git a/apps/browser/src/autofill/services/autofill-constants.ts b/apps/browser/src/autofill/services/autofill-constants.ts index 992551256bb..1bab7210672 100644 --- a/apps/browser/src/autofill/services/autofill-constants.ts +++ b/apps/browser/src/autofill/services/autofill-constants.ts @@ -38,6 +38,7 @@ export class AutoFillConstants { "mfacode", "otc", "otc-code", + "otp", "otp-code", "otpcode", "pin", diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index d62013b1611..98e58a022e4 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -105,7 +105,11 @@ export async function sendExtensionMessage( command: string, options: Record = {}, ): Promise { - if (typeof browser !== "undefined") { + if ( + typeof browser !== "undefined" && + typeof browser.runtime !== "undefined" && + typeof browser.runtime.sendMessage !== "undefined" + ) { return browser.runtime.sendMessage({ command, ...options }); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index cf393e0a44c..0da55cbda5f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -401,6 +401,8 @@ export default class MainBackground { const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) => await this.logout(logoutReason, userId); + const runtimeNativeMessagingBackground = () => this.nativeMessagingBackground; + const refreshAccessTokenErrorCallback = () => { // Send toast to popup this.messagingService.send("showToast", { @@ -616,7 +618,9 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); - this.biometricsService = new BackgroundBrowserBiometricsService(this.nativeMessagingBackground); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + ); this.kdfConfigService = new KdfConfigService(this.stateProvider); diff --git a/apps/browser/src/platform/services/background-browser-biometrics.service.ts b/apps/browser/src/platform/services/background-browser-biometrics.service.ts index 41ae15972cd..0cd48c45938 100644 --- a/apps/browser/src/platform/services/background-browser-biometrics.service.ts +++ b/apps/browser/src/platform/services/background-browser-biometrics.service.ts @@ -6,20 +6,20 @@ import { BrowserBiometricsService } from "./browser-biometrics.service"; @Injectable() export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { - constructor(private nativeMessagingBackground: NativeMessagingBackground) { + constructor(private nativeMessagingBackground: () => NativeMessagingBackground) { super(); } async authenticateBiometric(): Promise { - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); + const responsePromise = this.nativeMessagingBackground().getResponse(); + await this.nativeMessagingBackground().send({ command: "biometricUnlock" }); const response = await responsePromise; return response.response === "unlocked"; } async isBiometricUnlockAvailable(): Promise { - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlockAvailable" }); + const responsePromise = this.nativeMessagingBackground().getResponse(); + await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" }); const response = await responsePromise; return response.response === "available"; } diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 227ede146ba..3da6fdef196 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -199,6 +199,9 @@ export const routerTransition = trigger("routerTransition", [ transition("vault-settings => sync", inSlideLeft), transition("sync => vault-settings", outSlideRight), + transition("vault-settings => trash", inSlideLeft), + transition("trash => vault-settings", outSlideRight), + // Appearance settings transition("tabs => appearance", inSlideLeft), transition("appearance => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 455909336b3..82e673a9e54 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -91,6 +91,7 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit. import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { TrashComponent } from "../vault/popup/settings/trash.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; @@ -496,6 +497,12 @@ const routes: Routes = [ component: AccountSwitcherComponent, data: { state: "account-switcher", doNotSaveUrl: true }, }, + { + path: "trash", + component: TrashComponent, + canActivate: [authGuard], + data: { state: "trash" }, + }, ]; @Injectable() diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index b1e95afb535..7664c7e0ca1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -312,13 +312,13 @@ export class AddEditV2Component implements OnInit { switch (type) { case CipherType.Login: - return this.i18nService.t(partOne, this.i18nService.t("typeLogin")); + return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLocaleLowerCase()); case CipherType.Card: - return this.i18nService.t(partOne, this.i18nService.t("typeCard")); + return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLocaleLowerCase()); case CipherType.Identity: - return this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); + return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase()); case CipherType.SecureNote: - return this.i18nService.t(partOne, this.i18nService.t("note")); + return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase()); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index 487168539b9..f4444a10aeb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -13,7 +13,13 @@ - + + + + + + + + + + + + diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts new file mode 100644 index 00000000000..1ec0f52aa6d --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -0,0 +1,107 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { Router } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DialogService, + IconButtonModule, + ItemModule, + MenuModule, + SectionComponent, + SectionHeaderComponent, + ToastService, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-trash-list-items-container", + templateUrl: "trash-list-items-container.component.html", + standalone: true, + imports: [ + CommonModule, + ItemModule, + JslibModule, + SectionComponent, + SectionHeaderComponent, + MenuModule, + IconButtonModule, + ], +}) +export class TrashListItemsContainerComponent { + /** + * The list of trashed items to display. + */ + @Input() + ciphers: CipherView[] = []; + + @Input() + headerText: string; + + constructor( + private cipherService: CipherService, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private dialogService: DialogService, + private passwordRepromptService: PasswordRepromptService, + private router: Router, + ) {} + + async restore(cipher: CipherView) { + try { + await this.cipherService.restoreWithServer(cipher.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); + } catch (e) { + this.logService.error(e); + } + } + + async delete(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + + if (!repromptPassed) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { key: "permanentlyDeleteItemConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.cipherService.deleteWithServer(cipher.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + } catch (e) { + this.logService.error(e); + } + } + + async onViewCipher(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + if (!repromptPassed) { + return; + } + + await this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id, type: cipher.type }, + }); + } +} diff --git a/apps/browser/src/vault/popup/settings/trash.component.html b/apps/browser/src/vault/popup/settings/trash.component.html new file mode 100644 index 00000000000..ab3b6716504 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash.component.html @@ -0,0 +1,33 @@ + + + + + + + + + {{ "trashWarning" | i18n }} + + + + + + + + + + {{ "noItemsInTrash" | i18n }} + + + {{ "noItemsInTrashDesc" | i18n }} + + + + + diff --git a/apps/browser/src/vault/popup/settings/trash.component.ts b/apps/browser/src/vault/popup/settings/trash.component.ts new file mode 100644 index 00000000000..b6f77ef6a52 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash.component.ts @@ -0,0 +1,37 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CalloutModule, NoItemsModule } from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; + +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +import { VaultListItemsContainerComponent } from "../components/vault-v2/vault-list-items-container/vault-list-items-container.component"; +import { VaultPopupItemsService } from "../services/vault-popup-items.service"; + +import { TrashListItemsContainerComponent } from "./trash-list-items-container/trash-list-items-container.component"; + +@Component({ + templateUrl: "trash.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + VaultListItemsContainerComponent, + TrashListItemsContainerComponent, + CalloutModule, + NoItemsModule, + ], +}) +export class TrashComponent { + protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$; + + protected emptyTrashIcon = VaultIcons.EmptyTrash; + + constructor(private vaultPopupItemsService: VaultPopupItemsService) {} +} diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index 10243bdaa9f..03dd1182fbb 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -24,6 +24,12 @@ + + + {{ "trash" | i18n }} + + + - diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 226c92b45e3..436372c049b 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -1,43 +1,36 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.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-adjust-subscription", templateUrl: "adjust-subscription.component.html", }) -export class AdjustSubscription implements OnInit, OnDestroy { +export class AdjustSubscription { @Input() organizationId: string; @Input() maxAutoscaleSeats: number; @Input() currentSeatCount: number; @Input() seatPrice = 0; @Input() interval = "year"; @Output() onAdjusted = new EventEmitter(); - private destroy$ = new Subject(); adjustSubscriptionForm = this.formBuilder.group({ newSeatCount: [0, [Validators.min(0)]], limitSubscription: [false], newMaxSeats: [0, [Validators.min(0)]], }); - get limitSubscription(): boolean { - return this.adjustSubscriptionForm.value.limitSubscription; - } + constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, private toastService: ToastService, - ) {} - - ngOnInit() { + ) { this.adjustSubscriptionForm.patchValue({ newSeatCount: this.currentSeatCount, limitSubscription: this.maxAutoscaleSeats != null, @@ -45,7 +38,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { }); this.adjustSubscriptionForm .get("limitSubscription") - .valueChanges.pipe(takeUntil(this.destroy$)) + .valueChanges.pipe(takeUntilDestroyed()) .subscribe((value: boolean) => { if (value) { this.adjustSubscriptionForm @@ -63,10 +56,6 @@ export class AdjustSubscription implements OnInit, OnDestroy { }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } submit = async () => { this.adjustSubscriptionForm.markAllAsTouched(); if (this.adjustSubscriptionForm.invalid) { @@ -99,18 +88,15 @@ export class AdjustSubscription implements OnInit, OnDestroy { : 0; } - get additionalMaxSeatCount(): number { - return this.adjustSubscriptionForm.value.newMaxSeats - ? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount - : 0; - } - get maxSeatTotal(): number { return Math.abs((this.adjustSubscriptionForm.value.newMaxSeats ?? 0) * this.seatPrice); } get seatTotalCost(): number { - const totalSeat = Math.abs(this.adjustSubscriptionForm.value.newSeatCount * this.seatPrice); - return totalSeat; + return Math.abs(this.adjustSubscriptionForm.value.newSeatCount * this.seatPrice); + } + + get limitSubscription(): boolean { + return this.adjustSubscriptionForm.value.limitSubscription; } } diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 35cb0c2ac79..656796b443e 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -222,8 +222,10 @@ -

{{ "selfHostingTitle" | i18n }}

-

+

+ {{ "selfHostingTitle" | i18n }} +

+

{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }} {{ "downloadLicense" | i18n }} diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index b8616ae1b42..f28933a4ecc 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -345,6 +345,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy ); } + shownSelfHost(): boolean { + return ( + this.sub?.plan.productTier !== ProductTierType.Teams && + this.sub?.plan.productTier !== ProductTierType.Free + ); + } + cancelSubscription = async () => { const reference = openOffboardingSurvey(this.dialogService, { data: { diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index fa5cd4bf4e9..2f9e84fc6d5 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -75,7 +75,7 @@ diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 2f294a758db..c72daeffff4 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -47,7 +47,7 @@ (click)="bulkEditCollectionAccess()" > - {{ "access" | i18n }} + {{ "editAccess" | i18n }} diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index ed9dfbaf271..cd52e41e38d 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -11,7 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-collections", @@ -29,6 +29,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On accountService: AccountService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: CollectionsDialogParams, + toastService: ToastService, ) { super( collectionService, @@ -39,6 +40,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On logService, configService, accountService, + toastService, ); this.cipherId = params?.cipherId; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index bdcd409dbf8..2ff3e953ae7 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -15,7 +15,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 { SyncService } from "@bitwarden/common/platform/sync"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { EnrollMasterPasswordReset } from "../../../../admin-console/organizations/users/enroll-master-password-reset.component"; @@ -50,6 +50,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private dialogService: DialogService, private resetPasswordService: OrganizationUserResetPasswordService, private userVerificationService: UserVerificationService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -158,6 +159,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { this.syncService, this.logService, this.userVerificationService, + this.toastService, ); } else { // Remove reset password diff --git a/apps/web/src/app/vault/individual-vault/view.component.html b/apps/web/src/app/vault/individual-vault/view.component.html index a70f1be49d7..d1caf76192d 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.html +++ b/apps/web/src/app/vault/individual-vault/view.component.html @@ -1,4 +1,4 @@ - + {{ cipherTypeString }} diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index e0c0ce91a7b..72816d53214 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -15,7 +15,7 @@ import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherCollectionsRequest } from "@bitwarden/common/vault/models/request/cipher-collections.request"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { CollectionsComponent as BaseCollectionsComponent, @@ -41,6 +41,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { accountService: AccountService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams, + toastService: ToastService, ) { super( collectionService, @@ -53,6 +54,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { accountService, dialogRef, params, + toastService, ); this.allowSelectNone = true; this.collectionIds = params?.collectionIds; diff --git a/apps/web/src/app/vault/utils/collection-utils.spec.ts b/apps/web/src/app/vault/utils/collection-utils.spec.ts index bf84b3155b5..916ed8ff327 100644 --- a/apps/web/src/app/vault/utils/collection-utils.spec.ts +++ b/apps/web/src/app/vault/utils/collection-utils.spec.ts @@ -24,5 +24,16 @@ describe("CollectionUtils Service", () => { expect(result[0].node.name).toBe("Parent"); expect(result[0].children[0].node.name).toBe("Child"); }); + + it("should return an empty array if no collections are provided", () => { + // Arrange + const collections: CollectionView[] = []; + + // Act + const result = getNestedCollectionTree(collections); + + // Assert + expect(result).toEqual([]); + }); }); }); diff --git a/apps/web/src/app/vault/utils/collection-utils.ts b/apps/web/src/app/vault/utils/collection-utils.ts index cb995fd8ffb..b035c40f9f5 100644 --- a/apps/web/src/app/vault/utils/collection-utils.ts +++ b/apps/web/src/app/vault/utils/collection-utils.ts @@ -14,6 +14,10 @@ export function getNestedCollectionTree(collections: CollectionView[]): TreeNode export function getNestedCollectionTree( collections: (CollectionView | CollectionAdminView)[], ): TreeNode[] { + if (!collections) { + return []; + } + // Collections need to be cloned because ServiceUtils.nestedTraverse actively // modifies the names of collections. // These changes risk affecting collections store in StateService. diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 76d16036579..293a8cd5052 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9025,5 +9025,8 @@ }, "additionalContentAvailable": { "message": "Additional content is available" + }, + "editAccess": { + "message": "Edit access" } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index 5f000142527..de7642aa347 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { TableDataSource, NoItemsModule } from "@bitwarden/components"; +import { TableDataSource, NoItemsModule, ToastService } from "@bitwarden/components"; import { Devices } from "@bitwarden/web-vault/app/admin-console/icons"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; @@ -54,6 +54,7 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy { private logService: LogService, private validationService: ValidationService, private configService: ConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -84,17 +85,17 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy { authRequest, ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("loginRequestApproved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("loginRequestApproved"), + }); } catch (error) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("resetPasswordDetailsError"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("resetPasswordDetailsError"), + }); } }); } @@ -109,18 +110,22 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy { this.organizationId, this.tableDataSource.data, ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("allLoginRequestsApproved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("allLoginRequestsApproved"), + }); }); } async denyRequest(requestId: string) { await this.performAsyncAction(async () => { await this.organizationAuthRequestService.denyPendingRequests(this.organizationId, requestId); - this.platformUtilsService.showToast("error", null, this.i18nService.t("loginRequestDenied")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("loginRequestDenied"), + }); }); } @@ -134,11 +139,11 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy { this.organizationId, ...this.tableDataSource.data.map((r) => r.id), ); - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("allLoginRequestsDenied"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("allLoginRequestsDenied"), + }); }); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index 52e46915e9f..e0b76c7f5c3 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -13,7 +13,7 @@ import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitw import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { domainNameValidator } from "./validators/domain-name.validator"; import { uniqueInArrayValidator } from "./validators/unique-in-array.validator"; @@ -66,6 +66,7 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { private orgDomainService: OrgDomainServiceAbstraction, private validationService: ValidationService, private dialogService: DialogService, + private toastService: ToastService, ) {} // Angular Method Implementations @@ -112,7 +113,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { // Creates a new domain record. The DNS TXT Record will be generated server-side and returned in the response. saveDomain = async (): Promise => { if (this.domainForm.invalid) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("domainFormInvalid"), + }); return; } @@ -126,7 +131,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { this.data.orgDomain = await this.orgDomainApiService.post(this.data.organizationId, request); // Patch the DNS TXT Record that was generated server-side this.domainForm.controls.txt.patchValue(this.data.orgDomain.txt); - this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("domainSaved"), + }); } catch (e) { this.handleDomainSaveError(e); } @@ -177,7 +186,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { verifyDomain = async (): Promise => { if (this.domainForm.invalid) { // Note: shouldn't be possible, but going to leave this to be safe. - this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("domainFormInvalid"), + }); return; } @@ -188,7 +201,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { ); if (this.data.orgDomain.verifiedDate) { - this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("domainVerified"), + }); this.dialogRef.close(); } else { this.domainNameCtrl.setErrors({ @@ -250,7 +267,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { } await this.orgDomainApiService.delete(this.data.organizationId, this.data.orgDomain.id); - this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("domainRemoved"), + }); this.dialogRef.close(); }; diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index 27fede30f18..bc68bdaaf54 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -10,7 +10,7 @@ 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { DomainAddEditDialogComponent, @@ -37,6 +37,7 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { private orgDomainService: OrgDomainServiceAbstraction, private dialogService: DialogService, private validationService: ValidationService, + private toastService: ToastService, ) {} // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -110,13 +111,17 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { ); if (orgDomain.verifiedDate) { - this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("domainVerified"), + }); } else { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("domainNotVerified", domainName), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("domainNotVerified", domainName), + }); // Update this item so the last checked date gets updated. await this.updateOrgDomain(orgDomainId); } @@ -138,11 +143,11 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { switch (errorResponse.statusCode) { case HttpStatusCode.Conflict: if (errorResponse.message.includes("The domain is not available to be claimed")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("domainNotAvailable", domainName), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("domainNotAvailable", domainName), + }); } break; @@ -166,7 +171,11 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { await this.orgDomainApiService.delete(this.organizationId, orgDomainId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("domainRemoved"), + }); } ngOnDestroy(): void { diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index 55ae318e982..76e3caa145f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -17,7 +17,7 @@ import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/ 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-org-manage-scim", @@ -46,6 +46,7 @@ export class ScimComponent implements OnInit { private environmentService: EnvironmentService, private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -104,7 +105,11 @@ export class ScimComponent implements OnInit { endpointUrl: await this.getScimEndpointUrl(), clientSecret: response.apiKey, }); - this.platformUtilsService.showToast("success", null, this.i18nService.t("scimApiKeyRotated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("scimApiKeyRotated"), + }); }; copyScimKey = async () => { @@ -131,7 +136,11 @@ export class ScimComponent implements OnInit { } await this.setConnectionFormValues(response); - this.platformUtilsService.showToast("success", null, this.i18nService.t("scimSettingsSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("scimSettingsSaved"), + }); }; async getScimEndpointUrl() { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts index b255593ed20..dfbf1794f15 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts @@ -7,7 +7,7 @@ import { Provider } from "@bitwarden/common/admin-console/models/domain/provider import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebProviderService } from "../services/web-provider.service"; @@ -32,6 +32,7 @@ export class AddOrganizationComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private validationService: ValidationService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -73,11 +74,11 @@ export class AddOrganizationComponent implements OnInit { return; } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("organizationJoinedProvider"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("organizationJoinedProvider"), + }); this.dialogRef.close(true); }; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.spec.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.spec.ts index 7f49c42fb8a..a92db7edd14 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.spec.ts @@ -6,7 +6,7 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; 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 { providerPermissionsGuard } from "./provider-permissions.guard"; @@ -39,7 +39,7 @@ describe("Provider Permissions Guard", () => { TestBed.configureTestingModule({ providers: [ { provide: ProviderService, useValue: providerService }, - { provide: PlatformUtilsService, useValue: mock() }, + { provide: ToastService, useValue: mock() }, { provide: I18nService, useValue: mock() }, { provide: Router, useValue: mock() }, ], diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.ts index 6dcaf604666..dd4db2528b6 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/guards/provider-permissions.guard.ts @@ -9,7 +9,7 @@ import { import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; /** * `CanActivateFn` that asserts the logged in user has permission to access @@ -36,8 +36,8 @@ export function providerPermissionsGuard( return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { const providerService = inject(ProviderService); const router = inject(Router); - const platformUtilsService = inject(PlatformUtilsService); const i18nService = inject(I18nService); + const toastService = inject(ToastService); const provider = await providerService.get(route.params.providerId); if (provider == null) { @@ -45,14 +45,22 @@ export function providerPermissionsGuard( } if (!provider.isProviderAdmin && !provider.enabled) { - platformUtilsService.showToast("error", null, i18nService.t("providerIsDisabled")); + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("providerIsDisabled"), + }); return router.createUrlTree(["/"]); } const hasSpecifiedPermissions = permissionsCallback == null || permissionsCallback(provider); if (!hasSpecifiedPermissions) { - platformUtilsService.showToast("error", null, i18nService.t("accessDenied")); + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("accessDenied"), + }); return router.createUrlTree(["/providers", provider.id]); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 7db8d1e226f..0517715d425 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -9,6 +9,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil 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"; import { BaseEventsComponent } from "@bitwarden/web-vault/app/admin-console/common/base.events.component"; import { EventService } from "@bitwarden/web-vault/app/core"; import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export"; @@ -37,6 +38,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { logService: LogService, private userNamePipe: UserNamePipe, fileDownloadService: FileDownloadService, + toastService: ToastService, ) { super( eventService, @@ -45,6 +47,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { platformUtilsService, logService, fileDownloadService, + toastService, ); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 3e9856638e8..f78bccd3548 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -190,7 +190,7 @@ export class MembersComponent extends BaseMembersComponent { await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); } - deleteUser = (id: string): Promise => + removeUser = (id: string): Promise => this.apiService.deleteProviderUser(this.providerId, id); edit = async (user: ProviderUser | null): Promise => { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 1849809df5f..49961e0c7fc 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -21,7 +21,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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BasePeopleComponent } from "@bitwarden/web-vault/app/admin-console/common/base.people.component"; import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; @@ -75,6 +75,7 @@ export class PeopleComponent dialogService: DialogService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, private configService: ConfigService, + protected toastService: ToastService, ) { super( apiService, @@ -89,6 +90,7 @@ export class PeopleComponent userNamePipe, dialogService, organizationManagementPreferencesService, + toastService, ); } @@ -213,11 +215,11 @@ export class PeopleComponent const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited); if (filteredUsers.length <= 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("noSelectedUsersApplicable"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); return; } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts index 7406098ee4f..fde45224ab4 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts @@ -8,7 +8,7 @@ import { ProviderUserUpdateRequest } from "@bitwarden/common/admin-console/model 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"; /** * @deprecated Please use the {@link MembersDialogComponent} instead. @@ -42,6 +42,7 @@ export class UserAddEditComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private logService: LogService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -80,11 +81,11 @@ export class UserAddEditComponent implements OnInit { this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request); } await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name), + }); this.savedUser.emit(); } catch (e) { this.logService.error(e); @@ -109,11 +110,11 @@ export class UserAddEditComponent implements OnInit { try { this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId); await this.deletePromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.name), + }); this.deletedUser.emit(); } catch (e) { this.logService.error(e); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 01e863a826a..d5d7634db4c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "provider-account", @@ -49,6 +49,7 @@ export class AccountComponent implements OnDestroy, OnInit { private providerApiService: ProviderApiServiceAbstraction, private formBuilder: FormBuilder, private router: Router, + private toastService: ToastService, ) {} async ngOnInit() { @@ -86,7 +87,11 @@ export class AccountComponent implements OnDestroy, OnInit { await this.providerApiService.putProvider(this.providerId, request); await this.syncService.fullSync(true); this.provider = await this.providerApiService.getProvider(this.providerId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("providerUpdated"), + }); }; async deleteProvider() { @@ -110,11 +115,11 @@ export class AccountComponent implements OnDestroy, OnInit { try { await this.providerApiService.deleteProvider(this.providerId); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("providerDeleted"), - this.i18nService.t("providerDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("providerDeleted"), + message: this.i18nService.t("providerDeletedDesc"), + }); } catch (e) { this.logService.error(e); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 72b1d33b772..f231a273fc5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -6,6 +6,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil 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"; import { BaseEventsComponent } from "@bitwarden/web-vault/app/admin-console/common/base.events.component"; import { EventService } from "@bitwarden/web-vault/app/core"; import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export"; @@ -33,6 +34,7 @@ export class ServiceAccountEventsComponent platformUtilsService: PlatformUtilsService, logService: LogService, fileDownloadService: FileDownloadService, + toastService: ToastService, ) { super( eventService, @@ -41,6 +43,7 @@ export class ServiceAccountEventsComponent platformUtilsService, logService, fileDownloadService, + toastService, ); } diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index f185bed7e4a..4f166286184 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -14,6 +14,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { ToastService } from "@bitwarden/components"; @Directive() export class CollectionsComponent implements OnInit { @@ -39,6 +40,7 @@ export class CollectionsComponent implements OnInit { private logService: LogService, private configService: ConfigService, private accountService: AccountService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -82,11 +84,11 @@ export class CollectionsComponent implements OnInit { }) .map((c) => c.id); if (!this.allowSelectNone && selectedCollectionIds.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("selectOneCollection"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectOneCollection"), + }); return false; } this.cipherDomain.collectionIds = selectedCollectionIds; @@ -94,10 +96,18 @@ export class CollectionsComponent implements OnInit { this.formPromise = this.saveCollections(); await this.formPromise; this.onSavedCollections.emit(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("editedItem"), + }); return true; } 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; } } 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 80088bf7f91..6487c0cf847 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 @@ -251,12 +251,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { return; } - this.loginEmailService.setEmail(this.data.userEmail); + this.loginEmailService.setLoginEmail(this.data.userEmail); await this.router.navigate(["/login-with-device"]); } async requestAdminApproval() { - this.loginEmailService.setEmail(this.data.userEmail); + this.loginEmailService.setLoginEmail(this.data.userEmail); await this.router.navigate(["/admin-approval-requested"]); } diff --git a/libs/angular/src/auth/components/hint.component.ts b/libs/angular/src/auth/components/hint.component.ts index 7a152efbb9f..f7ae1e4c182 100644 --- a/libs/angular/src/auth/components/hint.component.ts +++ b/libs/angular/src/auth/components/hint.component.ts @@ -1,5 +1,6 @@ import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -27,8 +28,8 @@ export class HintComponent implements OnInit { protected toastService: ToastService, ) {} - ngOnInit(): void { - this.email = this.loginEmailService.getEmail() ?? ""; + async ngOnInit(): Promise { + this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? ""; } async submit() { 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 452b5ceee1e..a89952e024f 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 @@ -93,13 +93,6 @@ export class LoginViaAuthRequestComponent ) { 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? - const navigation = this.router.getCurrentNavigation(); - if (navigation) { - this.email = this.loginEmailService.getEmail(); - } - // Gets signalR push notification // Only fires on approval to prevent enumeration this.authRequestService.authRequestPushNotification$ @@ -118,6 +111,7 @@ export class LoginViaAuthRequestComponent } async ngOnInit() { + this.email = await firstValueFrom(this.loginEmailService.loginEmail$); this.userAuthNStatus = await this.authService.getAuthStatus(); const matchOptions: IsActiveMatchOptions = { @@ -165,7 +159,7 @@ export class LoginViaAuthRequestComponent } else { // Standard auth request // TODO: evaluate if we can remove the setting of this.email in the constructor - this.email = this.loginEmailService.getEmail(); + this.email = await firstValueFrom(this.loginEmailService.loginEmail$); if (!this.email) { this.toastService.showToast({ diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 501d753a976..3b927a05716 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -304,7 +304,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, private async loadEmailSettings() { // Try to load from memory first - const email = this.loginEmailService.getEmail(); + const email = await firstValueFrom(this.loginEmailService.loginEmail$); const rememberEmail = this.loginEmailService.getRememberEmail(); if (email) { this.formGroup.controls.email.setValue(email); @@ -321,7 +321,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, } protected async saveEmailSettings() { - this.loginEmailService.setEmail(this.formGroup.value.email); + this.loginEmailService.setLoginEmail(this.formGroup.value.email); this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); await this.loginEmailService.saveEmailSettings(); } diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts index c05f491acd2..87e26bd2df1 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts @@ -15,8 +15,6 @@ import { Environment, } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ButtonModule } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` @@ -40,7 +38,6 @@ const decorators = (options: { applicationVersion?: string; clientType?: ClientType; hostName?: string; - themeType?: ThemeType; }) => { return [ componentWrapperDecorator( @@ -84,12 +81,6 @@ const decorators = (options: { getClientType: () => options.clientType || ClientType.Web, } as Partial, }, - { - provide: ThemeStateService, - useValue: { - selectedTheme$: of(options.themeType || ThemeType.Light), - } as Partial, - }, ], }), applicationConfig({ diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 082edf40630..39570dabc8b 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,6 +1,10 @@

diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index fc3026dad34..a40fafc5db9 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -5,13 +5,11 @@ import { firstValueFrom } from "rxjs"; import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { IconModule, Icon } from "../../../../components/src/icon"; import { SharedModule } from "../../../../components/src/shared"; import { TypographyModule } from "../../../../components/src/typography"; -import { BitwardenLogoPrimary, BitwardenLogoWhite } from "../icons"; -import { BitwardenShieldPrimary, BitwardenShieldWhite } from "../icons/bitwarden-shield.icon"; +import { BitwardenLogo, BitwardenShield } from "../icons"; @Component({ standalone: true, @@ -34,20 +32,17 @@ export class AnonLayoutComponent implements OnInit, OnChanges { */ @Input() maxWidth: "md" | "3xl" = "md"; - protected logo: Icon; - + protected logo = BitwardenLogo; protected year = "2024"; protected clientType: ClientType; protected hostname: string; protected version: string; - protected theme: string; protected hideYearAndVersion = false; constructor( private environmentService: EnvironmentService, private platformUtilsService: PlatformUtilsService, - private themeStateService: ThemeStateService, ) { this.year = new Date().getFullYear().toString(); this.clientType = this.platformUtilsService.getClientType(); @@ -56,41 +51,18 @@ export class AnonLayoutComponent implements OnInit, OnChanges { async ngOnInit() { this.maxWidth = this.maxWidth ?? "md"; - - this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); - - if (this.theme === "dark") { - this.logo = BitwardenLogoWhite; - } else { - this.logo = BitwardenLogoPrimary; - } - - await this.updateIcon(this.theme); - this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname(); this.version = await this.platformUtilsService.getApplicationVersion(); + + // If there is no icon input, then use the default icon + if (this.icon == null) { + this.icon = BitwardenShield; + } } async ngOnChanges(changes: SimpleChanges) { - if (changes.icon) { - const theme = await firstValueFrom(this.themeStateService.selectedTheme$); - await this.updateIcon(theme); - } - if (changes.maxWidth) { this.maxWidth = changes.maxWidth.currentValue ?? "md"; } } - - private async updateIcon(theme: string) { - if (this.icon == null) { - if (theme === "dark") { - this.icon = BitwardenShieldWhite; - } - - if (theme !== "dark") { - this.icon = BitwardenShieldPrimary; - } - } - } } diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index edf6c8d70a1..110bda7ce81 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -1,11 +1,10 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { ClientType } from "@bitwarden/common/enums"; 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 { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ButtonModule } from "../../../../components/src/button"; import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service"; @@ -47,12 +46,6 @@ export default { }).asObservable(), }, }, - { - provide: ThemeStateService, - useValue: { - selectedTheme$: of("light"), - }, - }, ], }), ], diff --git a/libs/auth/src/angular/icons/bitwarden-logo.icon.ts b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts index b9094befff1..2a1ae48526b 100644 --- a/libs/auth/src/angular/icons/bitwarden-logo.icon.ts +++ b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts @@ -1,17 +1,9 @@ import { svgIcon } from "@bitwarden/components"; -export const BitwardenLogoPrimary = svgIcon` - +export const BitwardenLogo = svgIcon` + Bitwarden - - - -`; - -export const BitwardenLogoWhite = svgIcon` - - Bitwarden - - + + `; diff --git a/libs/auth/src/angular/icons/bitwarden-shield.icon.ts b/libs/auth/src/angular/icons/bitwarden-shield.icon.ts index a81a9906a6b..86e3a0bb1b2 100644 --- a/libs/auth/src/angular/icons/bitwarden-shield.icon.ts +++ b/libs/auth/src/angular/icons/bitwarden-shield.icon.ts @@ -1,13 +1,7 @@ import { svgIcon } from "@bitwarden/components"; -export const BitwardenShieldPrimary = svgIcon` - - - -`; - -export const BitwardenShieldWhite = svgIcon` - - +export const BitwardenShield = svgIcon` + + `; diff --git a/libs/auth/src/common/abstractions/login-email.service.ts b/libs/auth/src/common/abstractions/login-email.service.ts index d4fbbaff840..496d890f162 100644 --- a/libs/auth/src/common/abstractions/login-email.service.ts +++ b/libs/auth/src/common/abstractions/login-email.service.ts @@ -1,29 +1,28 @@ import { Observable } from "rxjs"; export abstract class LoginEmailServiceAbstraction { + /** + * An observable that monitors the loginEmail in memory. + * The loginEmail is the email that is being used in the current login process. + */ + loginEmail$: Observable; /** * An observable that monitors the storedEmail on disk. * This will return null if an account is being added. */ storedEmail$: Observable; /** - * Gets the current email being used in the login process from memory. - * @returns A string of the email. + * Sets the loginEmail in memory. + * The loginEmail is the email that is being used in the current login process. */ - getEmail: () => string; - /** - * Sets the current email being used in the login process in memory. - * @param email The email to be set. - */ - setEmail: (email: string) => void; + setLoginEmail: (email: string) => Promise; /** * Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called. * @returns A boolean stating whether or not the email should be stored on disk. */ getRememberEmail: () => boolean; /** - * Sets in memory whether or not the email should be stored on disk when - * `saveEmailSettings` is called. + * Sets in memory whether or not the email should be stored on disk when `saveEmailSettings` is called. */ setRememberEmail: (value: boolean) => void; /** diff --git a/libs/auth/src/common/services/login-email/login-email.service.spec.ts b/libs/auth/src/common/services/login-email/login-email.service.spec.ts index 55e54c82f6e..8bb9b962eaf 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.spec.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.spec.ts @@ -43,7 +43,7 @@ describe("LoginEmailService", () => { describe("storedEmail$", () => { it("returns the stored email when not adding an account", async () => { - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(true); await sut.saveEmailSettings(); @@ -53,7 +53,7 @@ describe("LoginEmailService", () => { }); it("returns the stored email when not adding an account and the user has just logged in", async () => { - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(true); await sut.saveEmailSettings(); @@ -66,7 +66,7 @@ describe("LoginEmailService", () => { }); it("returns null when adding an account", async () => { - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(true); await sut.saveEmailSettings(); @@ -83,7 +83,7 @@ describe("LoginEmailService", () => { describe("saveEmailSettings", () => { it("saves the email when not adding an account", async () => { - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(true); await sut.saveEmailSettings(); @@ -95,7 +95,7 @@ describe("LoginEmailService", () => { it("clears the email when not adding an account and rememberEmail is false", async () => { storedEmailState.stateSubject.next("initialEmail@bitwarden.com"); - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(false); await sut.saveEmailSettings(); @@ -110,7 +110,7 @@ describe("LoginEmailService", () => { ["OtherUserId" as UserId]: AuthenticationStatus.Locked, }); - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(true); await sut.saveEmailSettings(); @@ -127,7 +127,7 @@ describe("LoginEmailService", () => { ["OtherUserId" as UserId]: AuthenticationStatus.Locked, }); - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(false); await sut.saveEmailSettings(); @@ -140,11 +140,11 @@ describe("LoginEmailService", () => { it("does not clear the email and rememberEmail after saving", async () => { // Browser uses these values to maintain the email between login and 2fa components so // we do not want to clear them too early. - sut.setEmail("userEmail@bitwarden.com"); + await sut.setLoginEmail("userEmail@bitwarden.com"); sut.setRememberEmail(true); await sut.saveEmailSettings(); - const result = sut.getEmail(); + const result = await firstValueFrom(sut.loginEmail$); expect(result).toBe("userEmail@bitwarden.com"); }); diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts index 7793d3e7ff6..bb89b412c51 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -8,21 +8,28 @@ import { GlobalState, KeyDefinition, LOGIN_EMAIL_DISK, + LOGIN_EMAIL_MEMORY, StateProvider, } from "../../../../../common/src/platform/state"; import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service"; +export const LOGIN_EMAIL = new KeyDefinition(LOGIN_EMAIL_MEMORY, "loginEmail", { + deserializer: (value: string) => value, +}); + export const STORED_EMAIL = new KeyDefinition(LOGIN_EMAIL_DISK, "storedEmail", { deserializer: (value: string) => value, }); export class LoginEmailService implements LoginEmailServiceAbstraction { - private email: string | null; private rememberEmail: boolean; // True if an account is currently being added through account switching private readonly addingAccount$: Observable; + private readonly loginEmailState: GlobalState; + loginEmail$: Observable; + private readonly storedEmailState: GlobalState; storedEmail$: Observable; @@ -31,6 +38,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { private authService: AuthService, private stateProvider: StateProvider, ) { + this.loginEmailState = this.stateProvider.getGlobal(LOGIN_EMAIL); this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL); // In order to determine if an account is being added, we check if any account is not logged out @@ -46,6 +54,8 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { }), ); + this.loginEmail$ = this.loginEmailState.state$; + this.storedEmail$ = this.storedEmailState.state$.pipe( switchMap(async (storedEmail) => { // When adding an account, we don't show the stored email @@ -57,12 +67,8 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { ); } - getEmail() { - return this.email; - } - - setEmail(email: string) { - this.email = email; + async setLoginEmail(email: string) { + await this.loginEmailState.update((_) => email); } getRememberEmail() { @@ -76,25 +82,27 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { // Note: only clear values on successful login or you are sure they are not needed. // Browser uses these values to maintain the email between login and 2fa components so // we do not want to clear them too early. - clearValues() { - this.email = null; + async clearValues() { + await this.setLoginEmail(null); this.rememberEmail = false; } async saveEmailSettings() { const addingAccount = await firstValueFrom(this.addingAccount$); + const email = await firstValueFrom(this.loginEmail$); + await this.storedEmailState.update((storedEmail) => { // If we're adding an account, only overwrite the stored email when rememberEmail is true if (addingAccount) { if (this.rememberEmail) { - return this.email; + return email; } return storedEmail; } // Saving with rememberEmail set to false will clear the stored email if (this.rememberEmail) { - return this.email; + return email; } return null; }); diff --git a/libs/common/src/admin-console/abstractions/organization-user/organization-user.service.ts b/libs/common/src/admin-console/abstractions/organization-user/organization-user.service.ts index 7058be08037..aada830f954 100644 --- a/libs/common/src/admin-console/abstractions/organization-user/organization-user.service.ts +++ b/libs/common/src/admin-console/abstractions/organization-user/organization-user.service.ts @@ -210,19 +210,19 @@ export abstract class OrganizationUserService { ): Promise; /** - * Delete an organization user + * Remove an organization user * @param organizationId - Identifier for the organization the user belongs to * @param id - Organization user identifier */ - abstract deleteOrganizationUser(organizationId: string, id: string): Promise; + abstract removeOrganizationUser(organizationId: string, id: string): Promise; /** - * Delete many organization users + * Remove many organization users * @param organizationId - Identifier for the organization the users belongs to - * @param ids - List of organization user identifiers to delete - * @return List of user ids, including both those that were successfully deleted and those that had an error + * @param ids - List of organization user identifiers to remove + * @return List of user ids, including both those that were successfully removed and those that had an error */ - abstract deleteManyOrganizationUsers( + abstract removeManyOrganizationUsers( organizationId: string, ids: string[], ): Promise>; diff --git a/libs/common/src/admin-console/services/organization-user/organization-user.service.implementation.ts b/libs/common/src/admin-console/services/organization-user/organization-user.service.implementation.ts index b66805a20b9..e3687691b6b 100644 --- a/libs/common/src/admin-console/services/organization-user/organization-user.service.implementation.ts +++ b/libs/common/src/admin-console/services/organization-user/organization-user.service.implementation.ts @@ -274,7 +274,7 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe ); } - deleteOrganizationUser(organizationId: string, id: string): Promise { + removeOrganizationUser(organizationId: string, id: string): Promise { return this.apiService.send( "DELETE", "/organizations/" + organizationId + "/users/" + id, @@ -284,7 +284,7 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe ); } - async deleteManyOrganizationUsers( + async removeManyOrganizationUsers( organizationId: string, ids: string[], ): Promise> { diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 32307203b29..47b7199b940 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -53,6 +53,7 @@ export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { web: "disk-local", }); +export const LOGIN_EMAIL_MEMORY = new StateDefinition("loginEmail", "memory"); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index b88912453d2..030aecc02c0 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -44,6 +44,8 @@ --color-text-code: 192 17 118; --color-text-headers: 2 15 102; + --color-marketing-logo: 23 93 220; + --tw-ring-offset-color: #ffffff; } @@ -95,6 +97,8 @@ --color-text-code: 240 141 199; --color-text-headers: 226 227 228; + --color-marketing-logo: 255 255 255; + --tw-ring-offset-color: #1f242e; } @@ -134,6 +138,8 @@ --color-text-alt2: 255 255 255; --color-text-code: 219 177 211; + --color-marketing-logo: 255 255 255; + --tw-ring-offset-color: #434c5e; } @@ -173,6 +179,8 @@ --color-text-alt2: 255 255 255; --color-text-code: 240 141 199; + --color-marketing-logo: 255 255 255; + --tw-ring-offset-color: #002b36; } diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 7b6d30115d8..ff08091fb80 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -72,6 +72,7 @@ module.exports = { alt3: rgba("--color-background-alt3"), alt4: rgba("--color-background-alt4"), }, + "marketing-logo": rgba("--color-marketing-logo"), }, textColor: { main: rgba("--color-text-main"), diff --git a/libs/importer/spec/msecure-csv-importer.spec.ts b/libs/importer/spec/msecure-csv-importer.spec.ts new file mode 100644 index 00000000000..903be3bcb18 --- /dev/null +++ b/libs/importer/spec/msecure-csv-importer.spec.ts @@ -0,0 +1,113 @@ +import { CipherType } from "@bitwarden/common/vault/enums"; + +import { MSecureCsvImporter } from "../src/importers/msecure-csv-importer"; + +describe("MSecureCsvImporter.parse", () => { + let importer: MSecureCsvImporter; + beforeEach(() => { + importer = new MSecureCsvImporter(); + }); + + it("should correctly parse credit card entries as Secret Notes", async () => { + const mockCsvData = + `myCreditCard|155089404,Credit Card,,,Card Number|12|41111111111111111,Expiration Date|11|05/2026,Security Code|9|123,Name on Card|0|John Doe,PIN|9|1234,Issuing Bank|0|Visa,Phone Number|4|,Billing Address|0|,`.trim(); + const result = await importer.parse(mockCsvData); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("myCreditCard"); + expect(cipher.type).toBe(CipherType.Card); + expect(cipher.card.number).toBe("41111111111111111"); + expect(cipher.card.expiration).toBe("05 / 2026"); + expect(cipher.card.code).toBe("123"); + expect(cipher.card.cardholderName).toBe("John Doe"); + expect(cipher.card.brand).toBe("Visa"); + }); + + it("should correctly parse login entries", async () => { + const mockCsvData = ` + Bitwarden|810974637,Login,,,Website|2|bitwarden.com,Username|7|bitwarden user,Password|8|bitpassword, + `.trim(); + + const result = await importer.parse(mockCsvData); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("Bitwarden"); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.login.username).toBe("bitwarden user"); + expect(cipher.login.password).toBe("bitpassword"); + expect(cipher.login.uris[0].uri).toContain("bitwarden.com"); + }); + + it("should correctly parse login entries with notes", async () => { + const mockCsvData = + `Example|188987444,Login,,This is a note |,Website|2|example2.com,Username|7|username || lol,Password|8|this is a password,`.trim(); + + const result = await importer.parse(mockCsvData); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("Example"); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.login.username).toBe("username || lol"); + expect(cipher.login.password).toBe("this is a password"); + expect(cipher.login.uris[0].uri).toContain("example2.com"); + expect(cipher.notes).toBe("This is a note |"); + }); + + it("should correctly parse login entries with a tag", async () => { + const mockCsvData = ` + Website with a tag|1401978655,Login,tag holding it,,Website|2|johndoe.com,Username|7|JohnDoeWebsite,Password|8|JohnDoePassword, + `.trim(); + + const result = await importer.parse(mockCsvData); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("Website with a tag"); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.login.username).toBe("JohnDoeWebsite"); + expect(cipher.login.password).toBe("JohnDoePassword"); + expect(cipher.login.uris[0].uri).toContain("johndoe.com"); + expect(cipher.notes).toBeNull(); + expect(result.folders[0].name).toContain("tag holding it"); + }); + + it("should handle multiple entries correctly", async () => { + const mockCsvData = + `myCreditCard|155089404,Credit Card,,,Card Number|12|41111111111111111,Expiration Date|11|05/2026,Security Code|9|123,Name on Card|0|John Doe,PIN|9|1234,Issuing Bank|0|Visa,Phone Number|4|,Billing Address|0|, +Bitwarden|810974637,Login,,,Website|2|bitwarden.com,Username|7|bitwarden user,Password|8|bitpassword, +Example|188987444,Login,,This is a note |,Website|2|example2.com,Username|7|username || lol,Password|8|this is a password, +Website with a tag|1401978655,Login,tag holding it,,Website|2|johndoe.com,Username|7|JohnDoeWebsite,Password|8|JohnDoePassword,`.trim(); + + const result = await importer.parse(mockCsvData); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(4); + + // Check first entry (Credit Card) + const cipher1 = result.ciphers[0]; + expect(cipher1.name).toBe("myCreditCard"); + expect(cipher1.type).toBe(CipherType.Card); + + // Check second entry (Login - Bitwarden) + const cipher2 = result.ciphers[1]; + expect(cipher2.name).toBe("Bitwarden"); + expect(cipher2.type).toBe(CipherType.Login); + + // Check third entry (Login with note - Example) + const cipher3 = result.ciphers[2]; + expect(cipher3.name).toBe("Example"); + expect(cipher3.type).toBe(CipherType.Login); + + // Check fourth entry (Login with tag - Website with a tag) + const cipher4 = result.ciphers[3]; + expect(cipher4.name).toBe("Website with a tag"); + expect(cipher4.type).toBe(CipherType.Login); + }); +}); diff --git a/libs/importer/src/importers/msecure-csv-importer.ts b/libs/importer/src/importers/msecure-csv-importer.ts index 788dfd1d4e2..e953ce3a8db 100644 --- a/libs/importer/src/importers/msecure-csv-importer.ts +++ b/libs/importer/src/importers/msecure-csv-importer.ts @@ -9,7 +9,7 @@ import { Importer } from "./importer"; export class MSecureCsvImporter extends BaseImporter implements Importer { parse(data: string): Promise { const result = new ImportResult(); - const results = this.parseCsv(data, false); + const results = this.parseCsv(data, false, { delimiter: "," }); if (results == null) { result.success = false; return Promise.resolve(result); @@ -21,17 +21,43 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { } const folderName = - this.getValueOrDefault(value[0], "Unassigned") !== "Unassigned" ? value[0] : null; + this.getValueOrDefault(value[2], "Unassigned") !== "Unassigned" ? value[2] : null; this.processFolder(result, folderName); const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(value[2], "--"); + cipher.name = this.getValueOrDefault(value[0].split("|")[0], "--"); if (value[1] === "Web Logins" || value[1] === "Login") { - cipher.login.uris = this.makeUriArray(value[4]); - cipher.login.username = this.getValueOrDefault(value[5]); - cipher.login.password = this.getValueOrDefault(value[6]); + cipher.login.username = this.getValueOrDefault(this.splitValueRetainingLastPart(value[5])); + cipher.login.uris = this.makeUriArray(this.splitValueRetainingLastPart(value[4])); + cipher.login.password = this.getValueOrDefault(this.splitValueRetainingLastPart(value[6])); cipher.notes = !this.isNullOrWhitespace(value[3]) ? value[3].split("\\n").join("\n") : null; + } else if (value[1] === "Credit Card") { + cipher.type = CipherType.Card; + cipher.card.number = this.getValueOrDefault(this.splitValueRetainingLastPart(value[4])); + + const [month, year] = this.getValueOrDefault( + this.splitValueRetainingLastPart(value[5]), + ).split("/"); + cipher.card.expMonth = month.trim(); + cipher.card.expYear = year.trim(); + cipher.card.code = this.getValueOrDefault(this.splitValueRetainingLastPart(value[6])); + cipher.card.cardholderName = this.getValueOrDefault( + this.splitValueRetainingLastPart(value[7]), + ); + cipher.card.brand = this.getValueOrDefault(this.splitValueRetainingLastPart(value[9])); + cipher.notes = + this.getValueOrDefault(value[8].split("|")[0]) + + ": " + + this.getValueOrDefault(this.splitValueRetainingLastPart(value[8]), "") + + "\n" + + this.getValueOrDefault(value[10].split("|")[0]) + + ": " + + this.getValueOrDefault(this.splitValueRetainingLastPart(value[10]), "") + + "\n" + + this.getValueOrDefault(value[11].split("|")[0]) + + ": " + + this.getValueOrDefault(this.splitValueRetainingLastPart(value[11]), ""); } else if (value.length > 3) { cipher.type = CipherType.SecureNote; cipher.secureNote = new SecureNoteView(); @@ -43,7 +69,11 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { } } - if (!this.isNullOrWhitespace(value[1]) && cipher.type !== CipherType.Login) { + if ( + !this.isNullOrWhitespace(value[1]) && + cipher.type !== CipherType.Login && + cipher.type !== CipherType.Card + ) { cipher.name = value[1] + ": " + cipher.name; } @@ -58,4 +88,11 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { result.success = true; return Promise.resolve(result); } + + // mSecure returns values separated by "|" where after the second separator is the value + // like "Password|8|myPassword", we want to keep the "myPassword" but also ensure that if + // the value contains any "|" it works fine + private splitValueRetainingLastPart(value: string) { + return value.split("|").slice(0, 2).concat(value.split("|").slice(2).join("|")).pop(); + } } diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index a675384ff9f..9b4bfdb5970 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -3,6 +3,15 @@ {{ "cardExpiredMessage" | i18n }} + +

+ {{ "noEditPermissions" | i18n }} +

+