@@ -74,7 +76,7 @@
{{ r.subTitle }}
-
+ |
|
- {{ "exposedXTimes" | i18n: (exposedPasswordMap.get(r.id) | number) }}
+ {{ "exposedXTimes" | i18n: (r.exposedXTimes | number) }}
|
diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts
index 8503174a937..13d2804c5e5 100644
--- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts
+++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts
@@ -11,12 +11,14 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherReportComponent } from "./cipher-report.component";
+
+type ReportResult = CipherView & { exposedXTimes: number };
+
@Component({
selector: "app-exposed-passwords-report",
templateUrl: "exposed-passwords-report.component.html",
})
export class ExposedPasswordsReportComponent extends CipherReportComponent implements OnInit {
- exposedPasswordMap = new Map();
disabled = true;
constructor(
@@ -44,12 +46,12 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
async setCiphers() {
const allCiphers = await this.getAllCiphers();
- const exposedPasswordCiphers: CipherView[] = [];
+ const exposedPasswordCiphers: ReportResult[] = [];
const promises: Promise[] = [];
this.filterStatus = [0];
- allCiphers.forEach((ciph: any) => {
- const { type, login, isDeleted, edit, viewPassword, id } = ciph;
+ allCiphers.forEach((ciph) => {
+ const { type, login, isDeleted, edit, viewPassword } = ciph;
if (
type !== CipherType.Login ||
login.password == null ||
@@ -63,8 +65,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
if (exposedCount > 0) {
- exposedPasswordCiphers.push(ciph);
- this.exposedPasswordMap.set(id, exposedCount);
+ const row = { ...ciph, exposedXTimes: exposedCount } as ReportResult;
+ exposedPasswordCiphers.push(row);
}
});
promises.push(promise);
@@ -72,6 +74,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
await Promise.all(promises);
this.filterCiphersByOrg(exposedPasswordCiphers);
+ this.dataSource.sort = { column: "exposedXTimes", direction: "desc" };
}
protected canManageCipher(c: CipherView): boolean {
From 931f86c9482dc67751add2da5d1db7512761f184 Mon Sep 17 00:00:00 2001
From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com>
Date: Wed, 18 Sep 2024 12:48:47 -0500
Subject: [PATCH 043/104] [PM- 9666] Implement edit item view individual vault
(#10553)
* Add initial vault cipher form for cipher edit.
* Add ability to add new cipher by type
* Add ability to save and clone cipher,
* Update canEditAllCiphers to take 1 argument.
* Add attachments button to add/edit dialog.
* Add semi-working attachment dialog.
* Add working attachment functionality.
* Remove debugging code.
* Add tests for new attachments dialog component.
* Add AddEditComponentV2 tests.
* Remove AddEditComponentV2 delete functionality.
* Remove unnecessary else statement.
* Launch password generation in new dialog when extension refresh enabled.
* Add tests for PasswordGeneratorComponent.
* Adjust password and attachments dialog sizes.
* run lint:fix
* Remove unnecessary form from button.
* Add missing provider in test.
* Remove password generation events.
* Add WebVaultGeneratorDialogComponent and WebCipherFormGenerationService
* Move and rename CipherFormQueryParams
* Use WebCipherFormGenerationService to launch password / user generation modals.
* Add WebVaultGeneratorDialogComponent tests.
* Remove unnecessary functionality and corresponding tests.
* Fix failing tests.
* Remove unused properties from AddEditComponentV2
* Pass CipherFormConfig to dialog.
* Clean up unused attachment dialog functionality.
* Update AddEdit cancel functionality to prevent navigating user.
* Make attachment dialog open a static method.
* Add addCipherV2 method and clean up tests.
* Remove changes to QueryParams.
* Add tests for WebCipherFormGenerationService
* Remove unused onCipherSaved method.
* Remove cipherSaved event.
* Remove unused password generator component
* Refactor to simplify editCipherId for extensionRefresh flag.
* Add additional comments to AddEditComponentV2.
* Simplify open vault generator dialog comment.
* Remove unused organizationService
* Remove unnecessary typecasting.
* Remove extensionRefreshEnabled and related.
* Remove slideIn animation
* Remove unused AddEditComponentV2 properties.
* Add back generic typing.
* Condesnse properties into single form config.
* Remove onDestroy and related code.
* Run prettier
* fix injection warning
* Handle cipher save.
* Redirect to vault on delete and make actions consistent.
* Update comment.
---
.../add-edit-v2.component.html | 34 ++++
.../add-edit-v2.component.spec.ts | 124 ++++++++++++
.../individual-vault/add-edit-v2.component.ts | 177 ++++++++++++++++++
.../attachments-v2.component.html | 19 ++
.../attachments-v2.component.spec.ts | 65 +++++++
.../attachments-v2.component.ts | 87 +++++++++
.../vault/individual-vault/vault.component.ts | 167 +++++++++++++++--
.../individual-vault/view.component.spec.ts | 4 +-
.../vault/individual-vault/view.component.ts | 8 +-
.../app/vault/org-vault/vault.component.ts | 3 +-
apps/web/src/locales/en/messages.json | 21 +++
.../cipher-attachments.component.ts | 5 +
.../login-details-section.component.spec.ts | 5 +
.../web-generator-dialog.component.html | 22 +++
.../web-generator-dialog.component.spec.ts | 125 +++++++++++++
.../web-generator-dialog.component.ts | 89 +++++++++
...web-cipher-form-generation.service.spec.ts | 88 +++++++++
.../web-cipher-form-generation.service.ts | 40 ++++
18 files changed, 1065 insertions(+), 18 deletions(-)
create mode 100644 apps/web/src/app/vault/individual-vault/add-edit-v2.component.html
create mode 100644 apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts
create mode 100644 apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts
create mode 100644 apps/web/src/app/vault/individual-vault/attachments-v2.component.html
create mode 100644 apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts
create mode 100644 apps/web/src/app/vault/individual-vault/attachments-v2.component.ts
create mode 100644 libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.html
create mode 100644 libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts
create mode 100644 libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts
create mode 100644 libs/vault/src/cipher-form/services/web-cipher-form-generation.service.spec.ts
create mode 100644 libs/vault/src/cipher-form/services/web-cipher-form-generation.service.ts
diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.html b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.html
new file mode 100644
index 00000000000..6aa3e610645
--- /dev/null
+++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.html
@@ -0,0 +1,34 @@
+
+
+ {{ headerText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts
new file mode 100644
index 00000000000..cd3fec73778
--- /dev/null
+++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts
@@ -0,0 +1,124 @@
+import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ActivatedRoute, Router } from "@angular/router";
+import { mock, MockProxy } from "jest-mock-extended";
+import { of } from "rxjs";
+
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
+import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
+import { DialogService } from "@bitwarden/components";
+import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
+import { CipherFormConfig, DefaultCipherFormConfigService } from "@bitwarden/vault";
+
+import { AddEditComponentV2 } from "./add-edit-v2.component";
+
+describe("AddEditComponentV2", () => {
+ let component: AddEditComponentV2;
+ let fixture: ComponentFixture;
+ let organizationService: MockProxy;
+ let policyService: MockProxy;
+ let billingAccountProfileStateService: MockProxy;
+ let activatedRoute: MockProxy;
+ let dialogRef: MockProxy>;
+ let dialogService: MockProxy;
+ let cipherService: MockProxy;
+ let messagingService: MockProxy;
+ let folderService: MockProxy;
+ let collectionService: MockProxy;
+
+ const mockParams = {
+ cloneMode: false,
+ cipherFormConfig: mock(),
+ };
+
+ beforeEach(async () => {
+ const mockOrganization: Organization = {
+ id: "org-id",
+ name: "Test Organization",
+ } as Organization;
+
+ organizationService = mock();
+ organizationService.organizations$ = of([mockOrganization]);
+
+ policyService = mock();
+ policyService.policyAppliesToActiveUser$.mockImplementation((policyType: PolicyType) =>
+ of(true),
+ );
+
+ billingAccountProfileStateService = mock();
+ billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
+
+ activatedRoute = mock();
+ activatedRoute.queryParams = of({});
+
+ dialogRef = mock>();
+ dialogService = mock();
+ messagingService = mock();
+ folderService = mock();
+ folderService.folderViews$ = of([]);
+ collectionService = mock();
+ collectionService.decryptedCollections$ = of([]);
+
+ const mockDefaultCipherFormConfigService = {
+ buildConfig: jest.fn().mockResolvedValue({
+ allowPersonal: true,
+ allowOrganization: true,
+ }),
+ };
+
+ await TestBed.configureTestingModule({
+ imports: [AddEditComponentV2],
+ providers: [
+ { provide: DIALOG_DATA, useValue: mockParams },
+ { provide: DialogRef, useValue: dialogRef },
+ { provide: I18nService, useValue: { t: jest.fn().mockReturnValue("login") } },
+ { provide: DialogService, useValue: dialogService },
+ { provide: CipherService, useValue: cipherService },
+ { provide: MessagingService, useValue: messagingService },
+ { provide: OrganizationService, useValue: organizationService },
+ { provide: Router, useValue: mock() },
+ { provide: ActivatedRoute, useValue: activatedRoute },
+ { provide: CollectionService, useValue: collectionService },
+ { provide: FolderService, useValue: folderService },
+ { provide: CryptoService, useValue: mock() },
+ { provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService },
+ { provide: PolicyService, useValue: policyService },
+ { provide: DefaultCipherFormConfigService, useValue: mockDefaultCipherFormConfigService },
+ {
+ provide: PasswordGenerationServiceAbstraction,
+ useValue: mock(),
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(AddEditComponentV2);
+ component = fixture.componentInstance;
+ });
+
+ describe("ngOnInit", () => {
+ it("initializes the component with cipher", async () => {
+ await component.ngOnInit();
+
+ expect(component).toBeTruthy();
+ });
+ });
+
+ describe("cancel", () => {
+ it("handles cancel action", async () => {
+ const spyClose = jest.spyOn(dialogRef, "close");
+
+ await component.cancel();
+
+ expect(spyClose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts
new file mode 100644
index 00000000000..64935c8af38
--- /dev/null
+++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts
@@ -0,0 +1,177 @@
+import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
+import { CommonModule } from "@angular/common";
+import { Component, Inject, OnInit } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { CipherId } from "@bitwarden/common/types/guid";
+import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { AsyncActionsModule, DialogModule, DialogService, ItemModule } from "@bitwarden/components";
+import {
+ CipherAttachmentsComponent,
+ CipherFormConfig,
+ CipherFormGenerationService,
+ CipherFormMode,
+ CipherFormModule,
+} from "@bitwarden/vault";
+
+import { WebCipherFormGenerationService } from "../../../../../../libs/vault/src/cipher-form/services/web-cipher-form-generation.service";
+import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
+import { SharedModule } from "../../shared/shared.module";
+
+import { AttachmentsV2Component } from "./attachments-v2.component";
+
+/**
+ * The result of the AddEditCipherDialogV2 component.
+ */
+export enum AddEditCipherDialogResult {
+ Edited = "edited",
+ Added = "added",
+ Canceled = "canceled",
+}
+
+/**
+ * The close result of the AddEditCipherDialogV2 component.
+ */
+export interface AddEditCipherDialogCloseResult {
+ /**
+ * The action that was taken.
+ */
+ action: AddEditCipherDialogResult;
+ /**
+ * The ID of the cipher that was edited or added.
+ */
+ id?: CipherId;
+}
+
+/**
+ * Component for viewing a cipher, presented in a dialog.
+ */
+@Component({
+ selector: "app-vault-add-edit-v2",
+ templateUrl: "add-edit-v2.component.html",
+ standalone: true,
+ imports: [
+ CipherViewComponent,
+ CommonModule,
+ AsyncActionsModule,
+ DialogModule,
+ SharedModule,
+ CipherFormModule,
+ CipherAttachmentsComponent,
+ ItemModule,
+ ],
+ providers: [{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }],
+})
+export class AddEditComponentV2 implements OnInit {
+ config: CipherFormConfig;
+ headerText: string;
+ canAccessAttachments: boolean = false;
+
+ /**
+ * Constructor for the AddEditComponentV2 component.
+ * @param params The parameters for the component.
+ * @param dialogRef The reference to the dialog.
+ * @param i18nService The internationalization service.
+ * @param dialogService The dialog service.
+ * @param billingAccountProfileStateService The billing account profile state service.
+ */
+ constructor(
+ @Inject(DIALOG_DATA) public params: CipherFormConfig,
+ private dialogRef: DialogRef,
+ private i18nService: I18nService,
+ private dialogService: DialogService,
+ private billingAccountProfileStateService: BillingAccountProfileStateService,
+ ) {
+ this.billingAccountProfileStateService.hasPremiumFromAnySource$
+ .pipe(takeUntilDestroyed())
+ .subscribe((canAccessPremium) => {
+ this.canAccessAttachments = canAccessPremium;
+ });
+ }
+
+ /**
+ * Lifecycle hook for component initialization.
+ */
+ async ngOnInit() {
+ this.config = this.params;
+ this.headerText = this.setHeader(this.config?.mode, this.config.cipherType);
+ }
+
+ /**
+ * Getter to check if the component is loading.
+ */
+ get loading() {
+ return this.config == null;
+ }
+
+ /**
+ * Method to handle cancel action. Called when a user clicks the cancel button.
+ */
+ async cancel() {
+ this.dialogRef.close({ action: AddEditCipherDialogResult.Canceled });
+ }
+
+ /**
+ * Sets the header text based on the mode and type of the cipher.
+ * @param mode The form mode.
+ * @param type The cipher type.
+ * @returns The header text.
+ */
+ setHeader(mode: CipherFormMode, type: CipherType) {
+ const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
+ switch (type) {
+ case CipherType.Login:
+ return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
+ case CipherType.Card:
+ return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
+ case CipherType.Identity:
+ return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
+ case CipherType.SecureNote:
+ return this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
+ }
+ }
+
+ /**
+ * Opens the attachments dialog.
+ */
+ async openAttachmentsDialog() {
+ this.dialogService.open(
+ AttachmentsV2Component,
+ {
+ data: {
+ cipherId: this.config.originalCipher?.id as CipherId,
+ },
+ },
+ );
+ }
+
+ /**
+ * Handles the event when a cipher is saved.
+ * @param cipherView The cipher view that was saved.
+ */
+ async onCipherSaved(cipherView: CipherView) {
+ this.dialogRef.close({
+ action:
+ this.config.mode === "add"
+ ? AddEditCipherDialogResult.Added
+ : AddEditCipherDialogResult.Edited,
+ id: cipherView.id as CipherId,
+ });
+ }
+}
+
+/**
+ * Strongly typed helper to open a cipher add/edit dialog
+ * @param dialogService Instance of the dialog service that will be used to open the dialog
+ * @param config Configuration for the dialog
+ * @returns A reference to the opened dialog
+ */
+export function openAddEditCipherDialog(
+ dialogService: DialogService,
+ config: DialogConfig,
+): DialogRef {
+ return dialogService.open(AddEditComponentV2, config);
+}
diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.html b/apps/web/src/app/vault/individual-vault/attachments-v2.component.html
new file mode 100644
index 00000000000..532a0224be4
--- /dev/null
+++ b/apps/web/src/app/vault/individual-vault/attachments-v2.component.html
@@ -0,0 +1,19 @@
+
+
+ {{ "attachments" | i18n }}
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts b/apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts
new file mode 100644
index 00000000000..8099d8fe929
--- /dev/null
+++ b/apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts
@@ -0,0 +1,65 @@
+import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { mock } from "jest-mock-extended";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { CipherId } from "@bitwarden/common/types/guid";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+
+import {
+ AttachmentsV2Component,
+ AttachmentDialogResult,
+ AttachmentsDialogParams,
+} from "./attachments-v2.component";
+
+describe("AttachmentsV2Component", () => {
+ let component: AttachmentsV2Component;
+ let fixture: ComponentFixture;
+
+ const mockCipherId: CipherId = "cipher-id" as CipherId;
+ const mockParams: AttachmentsDialogParams = {
+ cipherId: mockCipherId,
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AttachmentsV2Component, NoopAnimationsModule],
+ providers: [
+ { provide: DIALOG_DATA, useValue: mockParams },
+ { provide: DialogRef, useValue: mock() },
+ { provide: I18nService, useValue: mock() },
+ { provide: CipherService, useValue: mock() },
+ { provide: LogService, useValue: mock() },
+ { provide: AccountService, useValue: mock() },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(AttachmentsV2Component);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("initializes without errors and with the correct cipherId", () => {
+ expect(component).toBeTruthy();
+ expect(component.cipherId).toBe(mockParams.cipherId);
+ });
+
+ it("closes the dialog with 'uploaded' result on uploadSuccessful", () => {
+ const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close");
+
+ component.uploadSuccessful();
+
+ expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: AttachmentDialogResult.Uploaded });
+ });
+
+ it("closes the dialog with 'removed' result on removalSuccessful", () => {
+ const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close");
+
+ component.removalSuccessful();
+
+ expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: AttachmentDialogResult.Removed });
+ });
+});
diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts b/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts
new file mode 100644
index 00000000000..e3b974e6c09
--- /dev/null
+++ b/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts
@@ -0,0 +1,87 @@
+import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
+import { CommonModule } from "@angular/common";
+import { Component, Inject } from "@angular/core";
+
+import { CipherId } from "@bitwarden/common/types/guid";
+import { DialogService } from "@bitwarden/components";
+import { CipherAttachmentsComponent } from "@bitwarden/vault";
+
+import { SharedModule } from "../../shared";
+
+export interface AttachmentsDialogParams {
+ cipherId: CipherId;
+}
+
+/**
+ * Enum representing the possible results of the attachment dialog.
+ */
+export enum AttachmentDialogResult {
+ Uploaded = "uploaded",
+ Removed = "removed",
+ Closed = "closed",
+}
+
+export interface AttachmentDialogCloseResult {
+ action: AttachmentDialogResult;
+}
+
+/**
+ * Component for the attachments dialog.
+ */
+@Component({
+ selector: "app-vault-attachments-v2",
+ templateUrl: "attachments-v2.component.html",
+ standalone: true,
+ imports: [CommonModule, SharedModule, CipherAttachmentsComponent],
+})
+export class AttachmentsV2Component {
+ cipherId: CipherId;
+ attachmentFormId = CipherAttachmentsComponent.attachmentFormID;
+
+ /**
+ * Constructor for AttachmentsV2Component.
+ * @param dialogRef - Reference to the dialog.
+ * @param params - Parameters passed to the dialog.
+ */
+ constructor(
+ private dialogRef: DialogRef,
+ @Inject(DIALOG_DATA) public params: AttachmentsDialogParams,
+ ) {
+ this.cipherId = params.cipherId;
+ }
+
+ /**
+ * Opens the attachments dialog.
+ * @param dialogService - The dialog service.
+ * @param params - The parameters for the dialog.
+ * @returns The dialog reference.
+ */
+ static open(
+ dialogService: DialogService,
+ params: AttachmentsDialogParams,
+ ): DialogRef {
+ return dialogService.open(AttachmentsV2Component, {
+ data: params,
+ });
+ }
+
+ /**
+ * Called when an attachment is successfully uploaded.
+ * Closes the dialog with an 'uploaded' result.
+ */
+ uploadSuccessful() {
+ this.dialogRef.close({
+ action: AttachmentDialogResult.Uploaded,
+ });
+ }
+
+ /**
+ * Called when an attachment is successfully removed.
+ * Closes the dialog with a 'removed' result.
+ */
+ removalSuccessful() {
+ this.dialogRef.close({
+ action: AttachmentDialogResult.Removed,
+ });
+ }
+}
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts
index dcf62235d1b..a288b298bad 100644
--- a/apps/web/src/app/vault/individual-vault/vault.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault.component.ts
@@ -47,20 +47,25 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
-import { OrganizationId } from "@bitwarden/common/types/guid";
+import { CipherId, OrganizationId, CollectionId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
+import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { DialogService, Icons, ToastService } from "@bitwarden/components";
-import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault";
+import {
+ CollectionAssignmentResult,
+ DefaultCipherFormConfigService,
+ PasswordRepromptService,
+} from "@bitwarden/vault";
import { SharedModule } from "../../shared/shared.module";
import { AssignCollectionsWebComponent } from "../components/assign-collections";
@@ -74,7 +79,17 @@ import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { getNestedCollectionTree } from "../utils/collection-utils";
+import {
+ AddEditCipherDialogCloseResult,
+ AddEditCipherDialogResult,
+ openAddEditCipherDialog,
+} from "./add-edit-v2.component";
import { AddEditComponent } from "./add-edit.component";
+import {
+ AttachmentDialogCloseResult,
+ AttachmentDialogResult,
+ AttachmentsV2Component,
+} from "./attachments-v2.component";
import { AttachmentsComponent } from "./attachments.component";
import {
BulkDeleteDialogResult,
@@ -131,7 +146,11 @@ const SearchTextDebounceInterval = 200;
VaultItemsModule,
SharedModule,
],
- providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
+ providers: [
+ RoutedVaultFilterService,
+ RoutedVaultFilterBridgeService,
+ DefaultCipherFormConfigService,
+ ],
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
@@ -170,6 +189,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private searchText$ = new Subject();
private refresh$ = new BehaviorSubject(null);
private destroy$ = new Subject();
+ private extensionRefreshEnabled: boolean;
constructor(
private syncService: SyncService,
@@ -200,6 +220,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
private accountService: AccountService,
+ private cipherFormConfigService: DefaultCipherFormConfigService,
) {}
async ngOnInit() {
@@ -416,6 +437,11 @@ export class VaultComponent implements OnInit, OnDestroy {
this.refreshing = false;
},
);
+
+ // Check if the extension refresh feature flag is enabled
+ this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
+ FeatureFlag.ExtensionRefresh,
+ );
}
ngOnDestroy() {
@@ -511,6 +537,15 @@ export class VaultComponent implements OnInit, OnDestroy {
this.searchText$.next(searchText);
}
+ /**
+ * Handles opening the attachments dialog for a cipher.
+ * Runs several checks to ensure that the user has the correct permissions
+ * and then opens the attachments dialog.
+ * Uses the new AttachmentsV2Component if the extensionRefresh feature flag is enabled.
+ *
+ * @param cipher
+ * @returns
+ */
async editCipherAttachments(cipher: CipherView) {
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
this.go({ cipherId: null, itemId: null });
@@ -536,6 +571,24 @@ export class VaultComponent implements OnInit, OnDestroy {
);
let madeAttachmentChanges = false;
+
+ if (this.extensionRefreshEnabled) {
+ const dialogRef = AttachmentsV2Component.open(this.dialogService, {
+ cipherId: cipher.id as CipherId,
+ });
+
+ const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
+
+ if (
+ result.action === AttachmentDialogResult.Uploaded ||
+ result.action === AttachmentDialogResult.Removed
+ ) {
+ this.refresh();
+ }
+
+ return;
+ }
+
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
@@ -598,7 +651,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async addCipher(cipherType?: CipherType) {
- const component = await this.editCipher(null);
+ if (this.extensionRefreshEnabled) {
+ return this.addCipherV2(cipherType);
+ }
+
+ const component = (await this.editCipher(null)) as AddEditComponent;
component.type = cipherType || this.activeFilter.cipherType;
if (
this.activeFilter.organizationId !== "MyVault" &&
@@ -622,18 +679,60 @@ export class VaultComponent implements OnInit, OnDestroy {
component.folderId = this.activeFilter.folderId;
}
+ /**
+ * Opens the add cipher dialog.
+ * @param cipherType The type of cipher to add.
+ * @returns The dialog reference.
+ */
+ async addCipherV2(cipherType?: CipherType) {
+ const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
+ "add",
+ null,
+ cipherType,
+ );
+ cipherFormConfig.initialValues = {
+ organizationId:
+ this.activeFilter.organizationId !== "MyVault" && this.activeFilter.organizationId != null
+ ? (this.activeFilter.organizationId as OrganizationId)
+ : null,
+ collectionIds:
+ this.activeFilter.collectionId !== "AllCollections" &&
+ this.activeFilter.collectionId != null
+ ? [this.activeFilter.collectionId as CollectionId]
+ : [],
+ folderId: this.activeFilter.folderId,
+ };
+
+ // Open the dialog.
+ const dialogRef = openAddEditCipherDialog(this.dialogService, {
+ data: cipherFormConfig,
+ });
+
+ // Wait for the dialog to close.
+ const result: AddEditCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
+
+ // Refresh the vault to show the new cipher.
+ if (result?.action === AddEditCipherDialogResult.Added) {
+ this.refresh();
+ this.go({ itemId: result.id, action: "view" });
+ return;
+ }
+
+ // If the dialog was closed by any other action navigate back to the vault.
+ this.go({ cipherId: null, itemId: null, action: null });
+ }
+
async navigateToCipher(cipher: CipherView) {
this.go({ itemId: cipher?.id });
}
- async editCipher(cipher: CipherView) {
- return this.editCipherId(cipher?.id);
+ async editCipher(cipher: CipherView, cloneMode?: boolean) {
+ return this.editCipherId(cipher?.id, cloneMode);
}
- async editCipherId(id: string) {
+ async editCipherId(id: string, cloneMode?: boolean) {
const cipher = await this.cipherService.get(id);
- // if cipher exists (cipher is null when new) and MP reprompt
- // is on for this cipher, then show password reprompt
+
if (
cipher &&
cipher.reprompt !== 0 &&
@@ -644,6 +743,11 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
+ if (this.extensionRefreshEnabled) {
+ await this.editCipherIdV2(cipher, cloneMode);
+ return;
+ }
+
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
@@ -673,6 +777,46 @@ export class VaultComponent implements OnInit, OnDestroy {
return childComponent;
}
+ /**
+ * Edit a cipher using the new AddEditCipherDialogV2 component.
+ *
+ * @param cipher
+ * @param cloneMode
+ */
+ private async editCipherIdV2(cipher: Cipher, cloneMode?: boolean) {
+ const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
+ cloneMode ? "clone" : "edit",
+ cipher.id as CipherId,
+ cipher.type,
+ );
+
+ const dialogRef = openAddEditCipherDialog(this.dialogService, {
+ data: cipherFormConfig,
+ });
+
+ const result: AddEditCipherDialogCloseResult = await firstValueFrom(dialogRef.closed);
+
+ /**
+ * Refresh the vault if the dialog was closed by adding, editing, or deleting a cipher.
+ */
+ if (result?.action === AddEditCipherDialogResult.Edited) {
+ this.refresh();
+ }
+
+ /**
+ * View the cipher if the dialog was closed by editing the cipher.
+ */
+ if (result?.action === AddEditCipherDialogResult.Edited) {
+ this.go({ itemId: cipher.id, action: "view" });
+ return;
+ }
+
+ /**
+ * Navigate to the vault if the dialog was closed by any other action.
+ */
+ this.go({ cipherId: null, itemId: null, action: null });
+ }
+
/**
* Takes a CipherView and opens a dialog where it can be viewed (wraps viewCipherById).
* @param cipher - CipherView
@@ -718,8 +862,9 @@ export class VaultComponent implements OnInit, OnDestroy {
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
// If the dialog was closed by deleting the cipher, refresh the vault.
- if (result?.action === ViewCipherDialogResult.deleted) {
+ if (result?.action === ViewCipherDialogResult.Deleted) {
this.refresh();
+ this.go({ cipherId: null, itemId: null, action: null });
}
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
@@ -873,7 +1018,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
- const component = await this.editCipher(cipher);
+ const component = await this.editCipher(cipher, true);
component.cloneMode = true;
}
diff --git a/apps/web/src/app/vault/individual-vault/view.component.spec.ts b/apps/web/src/app/vault/individual-vault/view.component.spec.ts
index fec97e202ef..dfa2d50489b 100644
--- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts
+++ b/apps/web/src/app/vault/individual-vault/view.component.spec.ts
@@ -98,7 +98,7 @@ describe("ViewComponent", () => {
organizationId: mockCipher.organizationId,
},
});
- expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.edited });
+ expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.Edited });
});
});
@@ -111,7 +111,7 @@ describe("ViewComponent", () => {
await component.delete();
expect(deleteSpy).toHaveBeenCalled();
- expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.deleted });
+ expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.Deleted });
});
});
});
diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts
index fe317490c1f..964be0e8ab8 100644
--- a/apps/web/src/app/vault/individual-vault/view.component.ts
+++ b/apps/web/src/app/vault/individual-vault/view.component.ts
@@ -27,8 +27,8 @@ export interface ViewCipherDialogParams {
}
export enum ViewCipherDialogResult {
- edited = "edited",
- deleted = "deleted",
+ Edited = "edited",
+ Deleted = "deleted",
}
export interface ViewCipherDialogCloseResult {
@@ -117,7 +117,7 @@ export class ViewComponent implements OnInit, OnDestroy {
this.logService.error(e);
}
- this.dialogRef.close({ action: ViewCipherDialogResult.deleted });
+ this.dialogRef.close({ action: ViewCipherDialogResult.Deleted });
await this.router.navigate(["/vault"]);
};
@@ -137,7 +137,7 @@ export class ViewComponent implements OnInit, OnDestroy {
* Method to handle cipher editing. Called when a user clicks the edit button.
*/
async edit(): Promise {
- this.dialogRef.close({ action: ViewCipherDialogResult.edited });
+ this.dialogRef.close({ action: ViewCipherDialogResult.Edited });
await this.router.navigate([], {
queryParams: {
itemId: this.cipher.id,
diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts
index add1ecbe3e5..32f7f0eba5b 100644
--- a/apps/web/src/app/vault/org-vault/vault.component.ts
+++ b/apps/web/src/app/vault/org-vault/vault.component.ts
@@ -886,8 +886,9 @@ export class VaultComponent implements OnInit, OnDestroy {
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
// If the dialog was closed by deleting the cipher, refresh the vault.
- if (result.action === ViewCipherDialogResult.deleted) {
+ if (result.action === ViewCipherDialogResult.Deleted) {
this.refresh();
+ this.go({ cipherId: null, itemId: null, action: null });
}
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 1ca7d040821..87c0ad654fd 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -511,6 +511,24 @@
"viewItem": {
"message": "View item"
},
+ "newItemHeader": {
+ "message": "New $TYPE$",
+ "placeholders": {
+ "type": {
+ "content": "$1",
+ "example": "login"
+ }
+ }
+ },
+ "editItemHeader": {
+ "message": "Edit $TYPE$",
+ "placeholders": {
+ "type": {
+ "content": "$1",
+ "example": "login"
+ }
+ }
+ },
"viewItemType": {
"message": "View $ITEMTYPE$",
"placeholders": {
@@ -7399,6 +7417,9 @@
"fileUpload": {
"message": "File upload"
},
+ "upload": {
+ "message": "Upload"
+ },
"acceptedFormats": {
"message": "Accepted Formats:"
},
diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts
index a6febe48978..93cca2e5dbb 100644
--- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts
+++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts
@@ -85,6 +85,9 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
/** Emits after a file has been successfully uploaded */
@Output() onUploadSuccess = new EventEmitter();
+ /** Emits after a file has been successfully removed */
+ @Output() onRemoveSuccess = new EventEmitter();
+
cipher: CipherView;
attachmentForm: CipherAttachmentForm = this.formBuilder.group({
@@ -216,5 +219,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
if (index > -1) {
this.cipher.attachments.splice(index, 1);
}
+
+ this.onRemoveSuccess.emit();
}
}
diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts
index 569a6666999..232a4b2d27b 100644
--- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts
+++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts
@@ -7,6 +7,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -39,6 +40,8 @@ describe("LoginDetailsSectionComponent", () => {
let toastService: MockProxy;
let totpCaptureService: MockProxy;
let i18nService: MockProxy;
+ let configService: MockProxy;
+
const collect = jest.fn().mockResolvedValue(null);
beforeEach(async () => {
@@ -49,6 +52,7 @@ describe("LoginDetailsSectionComponent", () => {
toastService = mock();
totpCaptureService = mock();
i18nService = mock();
+ configService = mock();
collect.mockClear();
await TestBed.configureTestingModule({
@@ -60,6 +64,7 @@ describe("LoginDetailsSectionComponent", () => {
{ provide: ToastService, useValue: toastService },
{ provide: TotpCaptureService, useValue: totpCaptureService },
{ provide: I18nService, useValue: i18nService },
+ { provide: ConfigService, useValue: configService },
{ provide: EventCollectionService, useValue: { collect } },
],
})
diff --git a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.html b/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.html
new file mode 100644
index 00000000000..30259cd640c
--- /dev/null
+++ b/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.html
@@ -0,0 +1,22 @@
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
diff --git a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts
new file mode 100644
index 00000000000..844f15a47af
--- /dev/null
+++ b/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts
@@ -0,0 +1,125 @@
+import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
+
+import { UsernameGenerationServiceAbstraction } from "../../../../../../libs/tools/generator/extensions/legacy/src/username-generation.service.abstraction";
+import { CipherFormGeneratorComponent } from "../cipher-generator/cipher-form-generator.component";
+
+import {
+ WebVaultGeneratorDialogComponent,
+ WebVaultGeneratorDialogParams,
+ WebVaultGeneratorDialogAction,
+} from "./web-generator-dialog.component";
+
+describe("WebVaultGeneratorDialogComponent", () => {
+ let component: WebVaultGeneratorDialogComponent;
+ let fixture: ComponentFixture;
+
+ let dialogRef: MockProxy>;
+ let mockI18nService: MockProxy;
+ let passwordOptionsSubject: BehaviorSubject;
+ let usernameOptionsSubject: BehaviorSubject;
+ let mockPasswordGenerationService: MockProxy;
+ let mockUsernameGenerationService: MockProxy;
+
+ beforeEach(async () => {
+ dialogRef = mock>();
+ mockI18nService = mock();
+ passwordOptionsSubject = new BehaviorSubject([{ type: "password" }]);
+ usernameOptionsSubject = new BehaviorSubject([{ type: "username" }]);
+
+ mockPasswordGenerationService = mock();
+ mockPasswordGenerationService.getOptions$.mockReturnValue(
+ passwordOptionsSubject.asObservable(),
+ );
+
+ mockUsernameGenerationService = mock();
+ mockUsernameGenerationService.getOptions$.mockReturnValue(
+ usernameOptionsSubject.asObservable(),
+ );
+
+ const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" };
+
+ await TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, WebVaultGeneratorDialogComponent],
+ providers: [
+ {
+ provide: DialogRef,
+ useValue: dialogRef,
+ },
+ {
+ provide: DIALOG_DATA,
+ useValue: mockDialogData,
+ },
+ {
+ provide: I18nService,
+ useValue: mockI18nService,
+ },
+ {
+ provide: PlatformUtilsService,
+ useValue: mock(),
+ },
+ {
+ provide: PasswordGenerationServiceAbstraction,
+ useValue: mockPasswordGenerationService,
+ },
+ {
+ provide: UsernameGenerationServiceAbstraction,
+ useValue: mockUsernameGenerationService,
+ },
+ {
+ provide: CipherFormGeneratorComponent,
+ useValue: {
+ passwordOptions$: passwordOptionsSubject.asObservable(),
+ usernameOptions$: usernameOptionsSubject.asObservable(),
+ },
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(WebVaultGeneratorDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("initializes without errors", () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it("closes the dialog with 'canceled' result when close is called", () => {
+ const closeSpy = jest.spyOn(dialogRef, "close");
+
+ (component as any).close();
+
+ expect(closeSpy).toHaveBeenCalledWith({
+ action: WebVaultGeneratorDialogAction.Canceled,
+ });
+ });
+
+ it("closes the dialog with 'selected' result when selectValue is called", () => {
+ const closeSpy = jest.spyOn(dialogRef, "close");
+ const generatedValue = "generated-value";
+ component.onValueGenerated(generatedValue);
+
+ (component as any).selectValue();
+
+ expect(closeSpy).toHaveBeenCalledWith({
+ action: WebVaultGeneratorDialogAction.Selected,
+ generatedValue: generatedValue,
+ });
+ });
+
+ it("updates generatedValue when onValueGenerated is called", () => {
+ const generatedValue = "new-generated-value";
+ component.onValueGenerated(generatedValue);
+
+ expect((component as any).generatedValue).toBe(generatedValue);
+ });
+});
diff --git a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts b/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts
new file mode 100644
index 00000000000..03a41990c86
--- /dev/null
+++ b/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts
@@ -0,0 +1,89 @@
+import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
+import { CommonModule } from "@angular/common";
+import { Component, Inject } from "@angular/core";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { ButtonModule, DialogService } from "@bitwarden/components";
+import { CipherFormGeneratorComponent } from "@bitwarden/vault";
+
+import { DialogModule } from "../../../../../../libs/components/src/dialog";
+
+export interface WebVaultGeneratorDialogParams {
+ type: "password" | "username";
+}
+
+export interface WebVaultGeneratorDialogResult {
+ action: WebVaultGeneratorDialogAction;
+ generatedValue?: string;
+}
+
+export enum WebVaultGeneratorDialogAction {
+ Selected = "selected",
+ Canceled = "canceled",
+}
+
+@Component({
+ selector: "web-vault-generator-dialog",
+ templateUrl: "./web-generator-dialog.component.html",
+ standalone: true,
+ imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule],
+})
+export class WebVaultGeneratorDialogComponent {
+ protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
+ protected selectButtonText = this.i18nService.t(
+ this.isPassword ? "useThisPassword" : "useThisUsername",
+ );
+
+ /**
+ * Whether the dialog is generating a password/passphrase. If false, it is generating a username.
+ * @protected
+ */
+ protected get isPassword() {
+ return this.params.type === "password";
+ }
+
+ /**
+ * The currently generated value.
+ * @protected
+ */
+ protected generatedValue: string = "";
+
+ constructor(
+ @Inject(DIALOG_DATA) protected params: WebVaultGeneratorDialogParams,
+ private dialogRef: DialogRef,
+ private i18nService: I18nService,
+ ) {}
+
+ /**
+ * Close the dialog without selecting a value.
+ */
+ protected close = () => {
+ this.dialogRef.close({ action: WebVaultGeneratorDialogAction.Canceled });
+ };
+
+ /**
+ * Close the dialog and select the currently generated value.
+ */
+ protected selectValue = () => {
+ this.dialogRef.close({
+ action: WebVaultGeneratorDialogAction.Selected,
+ generatedValue: this.generatedValue,
+ });
+ };
+
+ onValueGenerated(value: string) {
+ this.generatedValue = value;
+ }
+
+ /**
+ * Opens the vault generator dialog.
+ */
+ static open(dialogService: DialogService, config: DialogConfig) {
+ return dialogService.open(
+ WebVaultGeneratorDialogComponent,
+ {
+ ...config,
+ },
+ );
+ }
+}
diff --git a/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.spec.ts b/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.spec.ts
new file mode 100644
index 00000000000..898ac8dcb7b
--- /dev/null
+++ b/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.spec.ts
@@ -0,0 +1,88 @@
+import { DialogRef } from "@angular/cdk/dialog";
+import { TestBed } from "@angular/core/testing";
+import { mock } from "jest-mock-extended";
+import { of } from "rxjs";
+
+import { DialogService } from "@bitwarden/components";
+
+import { WebVaultGeneratorDialogComponent } from "../components/web-generator-dialog/web-generator-dialog.component";
+
+import { WebCipherFormGenerationService } from "./web-cipher-form-generation.service";
+
+describe("WebCipherFormGenerationService", () => {
+ let service: WebCipherFormGenerationService;
+ let dialogService: jest.Mocked;
+ let closed = of({});
+ const close = jest.fn();
+ const dialogRef = {
+ close,
+ get closed() {
+ return closed;
+ },
+ } as unknown as DialogRef;
+
+ beforeEach(() => {
+ dialogService = mock();
+
+ TestBed.configureTestingModule({
+ providers: [
+ WebCipherFormGenerationService,
+ { provide: DialogService, useValue: dialogService },
+ ],
+ });
+
+ service = TestBed.inject(WebCipherFormGenerationService);
+ });
+
+ it("creates without error", () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe("generatePassword", () => {
+ it("opens the password generator dialog and returns the generated value", async () => {
+ const generatedValue = "generated-password";
+ closed = of({ action: "generated", generatedValue });
+ dialogService.open.mockReturnValue(dialogRef);
+
+ const result = await service.generatePassword();
+
+ expect(dialogService.open).toHaveBeenCalledWith(WebVaultGeneratorDialogComponent, {
+ data: { type: "password" },
+ });
+ expect(result).toBe(generatedValue);
+ });
+
+ it("returns null if the dialog is canceled", async () => {
+ closed = of({ action: "canceled" });
+ dialogService.open.mockReturnValue(dialogRef);
+
+ const result = await service.generatePassword();
+
+ expect(result).toBeNull();
+ });
+ });
+
+ describe("generateUsername", () => {
+ it("opens the username generator dialog and returns the generated value", async () => {
+ const generatedValue = "generated-username";
+ closed = of({ action: "generated", generatedValue });
+ dialogService.open.mockReturnValue(dialogRef);
+
+ const result = await service.generateUsername();
+
+ expect(dialogService.open).toHaveBeenCalledWith(WebVaultGeneratorDialogComponent, {
+ data: { type: "username" },
+ });
+ expect(result).toBe(generatedValue);
+ });
+
+ it("returns null if the dialog is canceled", async () => {
+ closed = of({ action: "canceled" });
+ dialogService.open.mockReturnValue(dialogRef);
+
+ const result = await service.generateUsername();
+
+ expect(result).toBeNull();
+ });
+ });
+});
diff --git a/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.ts b/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.ts
new file mode 100644
index 00000000000..cfa0b28dbf0
--- /dev/null
+++ b/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.ts
@@ -0,0 +1,40 @@
+import { inject, Injectable } from "@angular/core";
+import { firstValueFrom } from "rxjs";
+
+import { DialogService } from "@bitwarden/components";
+import { CipherFormGenerationService } from "@bitwarden/vault";
+
+import { WebVaultGeneratorDialogComponent } from "../components/web-generator-dialog/web-generator-dialog.component";
+
+@Injectable()
+export class WebCipherFormGenerationService implements CipherFormGenerationService {
+ private dialogService = inject(DialogService);
+
+ async generatePassword(): Promise {
+ const dialogRef = WebVaultGeneratorDialogComponent.open(this.dialogService, {
+ data: { type: "password" },
+ });
+
+ const result = await firstValueFrom(dialogRef.closed);
+
+ if (result == null || result.action === "canceled") {
+ return null;
+ }
+
+ return result.generatedValue;
+ }
+
+ async generateUsername(): Promise {
+ const dialogRef = WebVaultGeneratorDialogComponent.open(this.dialogService, {
+ data: { type: "username" },
+ });
+
+ const result = await firstValueFrom(dialogRef.closed);
+
+ if (result == null || result.action === "canceled") {
+ return null;
+ }
+
+ return result.generatedValue;
+ }
+}
From 1940256fe242da1b0d9edb98849b898169eac2da Mon Sep 17 00:00:00 2001
From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Date: Wed, 18 Sep 2024 18:55:31 +0100
Subject: [PATCH 044/104] Resolve the discount issue (#11128)
---
.../billing/organizations/change-plan-dialog.component.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html
index 1a7b390d21b..120b570cb1e 100644
--- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html
+++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html
@@ -21,8 +21,8 @@
>{{
"upgradeDiscount"
| i18n
- : (selectedInterval === planIntervals.Annually
- ? discountPercentageFromSub + this.discountPercentage
+ : (selectedInterval === planIntervals.Annually && discountPercentageFromSub == 0
+ ? this.discountPercentage
: this.discountPercentageFromSub)
}}
From 6c1d74a4cec9714ea0d3ad45697ec8422826dfd5 Mon Sep 17 00:00:00 2001
From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com>
Date: Wed, 18 Sep 2024 16:00:54 -0500
Subject: [PATCH 045/104] [PM-11395] [Defect] View Login - TOTP premium badge
does nothing when clicked (#10857)
* Add MessagingService to LoginCredentialView component.
* Add comments.
* Add WIP PremiumUpgradeService
* Simplify web PremiumUpgradeServices into one service.
* Relocate service files.
* Add browser version of PremiumUpgradePromptService.
* Cleanup debug comments.
* Run prettier.
* rework promptForPremium to take organization id and add test.
* Add test for browser
* Rework imports to fix linter errors.
* Add Shane's reworked WebVaultPremiumUpgradePromptService.
---
.../vault-v2/view-v2/view-v2.component.ts | 14 ++-
...ser-premium-upgrade-prompt.service.spec.ts | 26 +++++
.../browser-premium-upgrade-prompt.service.ts | 18 ++++
apps/web/src/app/app.component.ts | 2 +-
.../vault/individual-vault/view.component.ts | 8 +-
...web-premium-upgrade-prompt.service.spec.ts | 95 +++++++++++++++++++
.../web-premium-upgrade-prompt.service.ts | 57 +++++++++++
.../premium-upgrade-prompt.service.ts | 7 ++
.../login-credentials-view.component.html | 2 +-
.../login-credentials-view.component.ts | 8 +-
10 files changed, 225 insertions(+), 12 deletions(-)
create mode 100644 apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.spec.ts
create mode 100644 apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.ts
create mode 100644 apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts
create mode 100644 apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts
create mode 100644 libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts
diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts
index b2ef6701b42..a640abe69f6 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts
@@ -27,18 +27,22 @@ import {
ToastService,
} from "@bitwarden/components";
+import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
-
-import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
-import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
-import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component";
-import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
+import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
+import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
+import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
+import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
@Component({
selector: "app-view-v2",
templateUrl: "view-v2.component.html",
standalone: true,
+ providers: [
+ { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
+ ],
imports: [
CommonModule,
SearchModule,
diff --git a/apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.spec.ts b/apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.spec.ts
new file mode 100644
index 00000000000..9a00bacd6b0
--- /dev/null
+++ b/apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.spec.ts
@@ -0,0 +1,26 @@
+import { TestBed } from "@angular/core/testing";
+import { Router } from "@angular/router";
+import { mock, MockProxy } from "jest-mock-extended";
+
+import { BrowserPremiumUpgradePromptService } from "./browser-premium-upgrade-prompt.service";
+
+describe("BrowserPremiumUpgradePromptService", () => {
+ let service: BrowserPremiumUpgradePromptService;
+ let router: MockProxy;
+
+ beforeEach(async () => {
+ router = mock();
+ await TestBed.configureTestingModule({
+ providers: [BrowserPremiumUpgradePromptService, { provide: Router, useValue: router }],
+ }).compileComponents();
+
+ service = TestBed.inject(BrowserPremiumUpgradePromptService);
+ });
+
+ describe("promptForPremium", () => {
+ it("navigates to the premium update screen", async () => {
+ await service.promptForPremium();
+ expect(router.navigate).toHaveBeenCalledWith(["/premium"]);
+ });
+ });
+});
diff --git a/apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.ts b/apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.ts
new file mode 100644
index 00000000000..2909e3b3bd6
--- /dev/null
+++ b/apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.ts
@@ -0,0 +1,18 @@
+import { inject } from "@angular/core";
+import { Router } from "@angular/router";
+
+import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
+
+/**
+ * This class handles the premium upgrade process for the browser extension.
+ */
+export class BrowserPremiumUpgradePromptService implements PremiumUpgradePromptService {
+ private router = inject(Router);
+
+ async promptForPremium() {
+ /**
+ * Navigate to the premium update screen.
+ */
+ await this.router.navigate(["/premium"]);
+ }
+}
diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts
index ef6cbd2804a..1314670c44c 100644
--- a/apps/web/src/app/app.component.ts
+++ b/apps/web/src/app/app.component.ts
@@ -188,7 +188,7 @@ export class AppComponent implements OnDestroy, OnInit {
if (premiumConfirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.router.navigate(["settings/subscription/premium"]);
+ await this.router.navigate(["settings/subscription/premium"]);
}
break;
}
diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts
index 964be0e8ab8..fe846c9f64c 100644
--- a/apps/web/src/app/vault/individual-vault/view.component.ts
+++ b/apps/web/src/app/vault/individual-vault/view.component.ts
@@ -1,6 +1,6 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
-import { Component, EventEmitter, Inject, OnDestroy, OnInit } from "@angular/core";
+import { Component, Inject, OnInit, EventEmitter, OnDestroy } from "@angular/core";
import { Router } from "@angular/router";
import { Subject } from "rxjs";
@@ -19,8 +19,10 @@ import {
ToastService,
} from "@bitwarden/components";
+import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
import { SharedModule } from "../../shared/shared.module";
+import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
export interface ViewCipherDialogParams {
cipher: CipherView;
@@ -29,6 +31,7 @@ export interface ViewCipherDialogParams {
export enum ViewCipherDialogResult {
Edited = "edited",
Deleted = "deleted",
+ PremiumUpgrade = "premiumUpgrade",
}
export interface ViewCipherDialogCloseResult {
@@ -43,6 +46,9 @@ export interface ViewCipherDialogCloseResult {
templateUrl: "view.component.html",
standalone: true,
imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule],
+ providers: [
+ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
+ ],
})
export class ViewComponent implements OnInit, OnDestroy {
cipher: CipherView;
diff --git a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts
new file mode 100644
index 00000000000..6c68dae7077
--- /dev/null
+++ b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts
@@ -0,0 +1,95 @@
+import { DialogRef } from "@angular/cdk/dialog";
+import { TestBed } from "@angular/core/testing";
+import { Router } from "@angular/router";
+import { of, lastValueFrom } from "rxjs";
+
+import { OrganizationId } from "@bitwarden/common/types/guid";
+import { DialogService } from "@bitwarden/components";
+
+import {
+ ViewCipherDialogCloseResult,
+ ViewCipherDialogResult,
+} from "../individual-vault/view.component";
+
+import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
+
+describe("WebVaultPremiumUpgradePromptService", () => {
+ let service: WebVaultPremiumUpgradePromptService;
+ let dialogServiceMock: jest.Mocked;
+ let routerMock: jest.Mocked;
+ let dialogRefMock: jest.Mocked>;
+
+ beforeEach(() => {
+ dialogServiceMock = {
+ openSimpleDialog: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ routerMock = {
+ navigate: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ dialogRefMock = {
+ close: jest.fn(),
+ } as unknown as jest.Mocked>;
+
+ TestBed.configureTestingModule({
+ providers: [
+ WebVaultPremiumUpgradePromptService,
+ { provide: DialogService, useValue: dialogServiceMock },
+ { provide: Router, useValue: routerMock },
+ { provide: DialogRef, useValue: dialogRefMock },
+ ],
+ });
+
+ service = TestBed.inject(WebVaultPremiumUpgradePromptService);
+ });
+
+ it("prompts for premium upgrade and navigates to organization billing if organizationId is provided", async () => {
+ dialogServiceMock.openSimpleDialog.mockReturnValue(lastValueFrom(of(true)));
+ const organizationId = "test-org-id" as OrganizationId;
+
+ await service.promptForPremium(organizationId);
+
+ expect(dialogServiceMock.openSimpleDialog).toHaveBeenCalledWith({
+ title: { key: "upgradeOrganization" },
+ content: { key: "upgradeOrganizationDesc" },
+ acceptButtonText: { key: "upgradeOrganization" },
+ type: "info",
+ });
+ expect(routerMock.navigate).toHaveBeenCalledWith([
+ "organizations",
+ organizationId,
+ "billing",
+ "subscription",
+ ]);
+ expect(dialogRefMock.close).toHaveBeenCalledWith({
+ action: ViewCipherDialogResult.PremiumUpgrade,
+ });
+ });
+
+ it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
+ dialogServiceMock.openSimpleDialog.mockReturnValue(lastValueFrom(of(true)));
+
+ await service.promptForPremium();
+
+ expect(dialogServiceMock.openSimpleDialog).toHaveBeenCalledWith({
+ title: { key: "premiumRequired" },
+ content: { key: "premiumRequiredDesc" },
+ acceptButtonText: { key: "upgrade" },
+ type: "success",
+ });
+ expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
+ expect(dialogRefMock.close).toHaveBeenCalledWith({
+ action: ViewCipherDialogResult.PremiumUpgrade,
+ });
+ });
+
+ it("does not navigate or close dialog if upgrade is no action is taken", async () => {
+ dialogServiceMock.openSimpleDialog.mockReturnValue(lastValueFrom(of(false)));
+
+ await service.promptForPremium("test-org-id" as OrganizationId);
+
+ expect(routerMock.navigate).not.toHaveBeenCalled();
+ expect(dialogRefMock.close).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts
new file mode 100644
index 00000000000..8f9c8c0bd72
--- /dev/null
+++ b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts
@@ -0,0 +1,57 @@
+import { DialogRef } from "@angular/cdk/dialog";
+import { Injectable } from "@angular/core";
+import { Router } from "@angular/router";
+
+import { OrganizationId } from "@bitwarden/common/types/guid";
+import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
+import { DialogService } from "@bitwarden/components";
+
+import {
+ ViewCipherDialogCloseResult,
+ ViewCipherDialogResult,
+} from "../individual-vault/view.component";
+
+/**
+ * This service is used to prompt the user to upgrade to premium.
+ */
+@Injectable()
+export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePromptService {
+ constructor(
+ private dialogService: DialogService,
+ private router: Router,
+ private dialog: DialogRef,
+ ) {}
+
+ /**
+ * Prompts the user to upgrade to premium.
+ * @param organizationId The ID of the organization to upgrade.
+ */
+ async promptForPremium(organizationId?: OrganizationId) {
+ let upgradeConfirmed;
+ if (organizationId) {
+ upgradeConfirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "upgradeOrganization" },
+ content: { key: "upgradeOrganizationDesc" },
+ acceptButtonText: { key: "upgradeOrganization" },
+ type: "info",
+ });
+ if (upgradeConfirmed) {
+ await this.router.navigate(["organizations", organizationId, "billing", "subscription"]);
+ }
+ } else {
+ upgradeConfirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "premiumRequired" },
+ content: { key: "premiumRequiredDesc" },
+ acceptButtonText: { key: "upgrade" },
+ type: "success",
+ });
+ if (upgradeConfirmed) {
+ await this.router.navigate(["settings/subscription/premium"]);
+ }
+ }
+
+ if (upgradeConfirmed) {
+ this.dialog.close({ action: ViewCipherDialogResult.PremiumUpgrade });
+ }
+ }
+}
diff --git a/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts b/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts
new file mode 100644
index 00000000000..8733baaa471
--- /dev/null
+++ b/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts
@@ -0,0 +1,7 @@
+/**
+ * This interface defines the a contract for a service that prompts the user to upgrade to premium.
+ * It ensures that PremiumUpgradePromptService contains a promptForPremium method.
+ */
+export abstract class PremiumUpgradePromptService {
+ abstract promptForPremium(organizationId?: string): Promise;
+}
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html
index 88a59d9cc42..43be62f8c69 100644
--- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html
+++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html
@@ -97,7 +97,7 @@
bitBadge
variant="success"
class="tw-ml-2 tw-cursor-pointer"
- (click)="getPremium()"
+ (click)="getPremium(cipher.organizationId)"
slot="end"
>
{{ "premium" | i18n }}
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts
index c6957b1848e..b05d3318c3c 100644
--- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts
+++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts
@@ -1,6 +1,5 @@
import { CommonModule, DatePipe } from "@angular/common";
import { Component, inject, Input } from "@angular/core";
-import { Router } from "@angular/router";
import { Observable, shareReplay } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -20,6 +19,7 @@ import {
ColorPasswordModule,
} from "@bitwarden/components";
+import { PremiumUpgradePromptService } from "../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component";
import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
@@ -61,8 +61,8 @@ export class LoginCredentialsViewComponent {
constructor(
private billingAccountProfileStateService: BillingAccountProfileStateService,
- private router: Router,
private i18nService: I18nService,
+ private premiumUpgradeService: PremiumUpgradePromptService,
private eventCollectionService: EventCollectionService,
) {}
@@ -75,8 +75,8 @@ export class LoginCredentialsViewComponent {
return `${dateCreated} ${creationDate}`;
}
- async getPremium() {
- await this.router.navigate(["/premium"]);
+ async getPremium(organizationId?: string) {
+ await this.premiumUpgradeService.promptForPremium(organizationId);
}
async pwToggleValue(passwordVisible: boolean) {
From 2b85392b0fee0830d07fe8247972e842444a8458 Mon Sep 17 00:00:00 2001
From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com>
Date: Wed, 18 Sep 2024 16:02:47 -0500
Subject: [PATCH 046/104] PM-11338: [Defect] Clicking outside of View {Item}
dialog should close the dialog properly (#11035)
* Handle undefined result.
* Updated enum values for consistency.
---
apps/web/src/app/vault/org-vault/vault.component.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts
index 32f7f0eba5b..a2d8c29cbac 100644
--- a/apps/web/src/app/vault/org-vault/vault.component.ts
+++ b/apps/web/src/app/vault/org-vault/vault.component.ts
@@ -886,13 +886,13 @@ export class VaultComponent implements OnInit, OnDestroy {
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
// If the dialog was closed by deleting the cipher, refresh the vault.
- if (result.action === ViewCipherDialogResult.Deleted) {
+ if (result?.action === ViewCipherDialogResult.Deleted) {
this.refresh();
this.go({ cipherId: null, itemId: null, action: null });
}
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
- if (!result.action) {
+ if (!result?.action) {
this.go({ cipherId: null, itemId: null, action: null });
}
}
From 00f2317a8210f9ac86d7fd0739bd549055265de8 Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 18 Sep 2024 14:36:53 -0700
Subject: [PATCH 047/104] [PM-11903] - add file send component (#11132)
* wip - send file details
* wip - file send
* send file details
* fix click on send list container
* remove popup code
* remove popup code
* finalize send file details
* address PR feedback. add base form to send form
* revert changes to send list items container
* revert changes to send list items container
---------
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
---
apps/browser/src/_locales/en/messages.json | 3 +
.../add-edit/send-add-edit.component.ts | 19 ++--
.../base-send-details.component.ts | 2 +
.../send-details/send-details.component.html | 7 ++
.../send-details/send-details.component.ts | 2 +
.../send-file-details.component.html | 30 ++++++
.../send-file-details.component.ts | 92 +++++++++++++++++++
.../components/send-form.component.ts | 8 +-
.../src/send-form/send-form-container.ts | 4 +
9 files changed, 152 insertions(+), 15 deletions(-)
create mode 100644 libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.html
create mode 100644 libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index a57b65f6982..061929fc49c 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -1136,6 +1136,9 @@
"file": {
"message": "File"
},
+ "fileToShare": {
+ "message": "File to share"
+ },
"selectFile": {
"message": "Select a file"
},
diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts
index 48e6cbb8a31..49526bb032b 100644
--- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts
@@ -117,25 +117,18 @@ export class SendAddEditComponent {
)
.subscribe((config) => {
this.config = config;
- this.headerText = this.getHeaderText(config.mode, config.sendType);
+ this.headerText = this.getHeaderText(config.mode);
});
}
/**
- * Gets the header text based on the mode and type.
+ * Gets the header text based on the mode.
* @param mode The mode of the send form.
- * @param type The type of the send form.
* @returns The header text.
*/
- private getHeaderText(mode: SendFormMode, type: SendType) {
- const headerKey =
- mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
-
- switch (type) {
- case SendType.Text:
- return this.i18nService.t(headerKey, this.i18nService.t("sendTypeText"));
- case SendType.File:
- return this.i18nService.t(headerKey, this.i18nService.t("sendTypeFile"));
- }
+ private getHeaderText(mode: SendFormMode) {
+ return this.i18nService.t(
+ mode === "edit" || mode === "partial-edit" ? "editSend" : "createSend",
+ );
}
}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts
index 282f6caffab..f3e2229dd2b 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts
@@ -62,6 +62,8 @@ export class BaseSendDetailsComponent implements OnInit {
} as SendView);
});
});
+
+ this.sendFormContainer.registerChildForm("sendDetailsForm", this.sendDetailsForm);
}
async ngOnInit() {
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html
index e1f3f4f0d9f..47e1fc6059a 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html
@@ -16,6 +16,13 @@
[sendDetailsForm]="sendDetailsForm"
>
+
+
{{ "deletionDate" | i18n }}
+
+ {{ "file" | i18n }}
+ {{ originalSendView.file.fileName }}
+ {{ originalSendView.file.sizeName }}
+
+
+ {{ "fileToShare" | i18n }}
+
+
+ {{ fileName || ("noFileChosen" | i18n) }}
+
+
+ {{ "maxFileSize" | i18n }}
+
+
+
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts
new file mode 100644
index 00000000000..86c9fa96f1a
--- /dev/null
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts
@@ -0,0 +1,92 @@
+import { CommonModule } from "@angular/common";
+import { Component, Input, OnInit } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ Validators,
+ ReactiveFormsModule,
+ FormsModule,
+} from "@angular/forms";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
+import { SendFileView } from "@bitwarden/common/tools/send/models/view/send-file.view";
+import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
+import { ButtonModule, FormFieldModule, SectionComponent } from "@bitwarden/components";
+
+import { SendFormConfig } from "../../abstractions/send-form-config.service";
+import { SendFormContainer } from "../../send-form-container";
+
+import { BaseSendDetailsForm } from "./base-send-details.component";
+
+type BaseSendFileDetailsForm = FormGroup<{
+ file: FormControl;
+}>;
+
+export type SendFileDetailsForm = BaseSendFileDetailsForm & BaseSendDetailsForm;
+
+@Component({
+ selector: "tools-send-file-details",
+ templateUrl: "./send-file-details.component.html",
+ standalone: true,
+ imports: [
+ ButtonModule,
+ CommonModule,
+ JslibModule,
+ ReactiveFormsModule,
+ FormFieldModule,
+ SectionComponent,
+ FormsModule,
+ ],
+})
+export class SendFileDetailsComponent implements OnInit {
+ @Input() config: SendFormConfig;
+ @Input() originalSendView?: SendView;
+ @Input() sendDetailsForm: BaseSendDetailsForm;
+
+ baseSendFileDetailsForm: BaseSendFileDetailsForm;
+ sendFileDetailsForm: SendFileDetailsForm;
+
+ FileSendType = SendType.File;
+ fileName = "";
+
+ constructor(
+ private formBuilder: FormBuilder,
+ protected sendFormContainer: SendFormContainer,
+ ) {
+ this.baseSendFileDetailsForm = this.formBuilder.group({
+ file: this.formBuilder.control(null, Validators.required),
+ });
+
+ this.sendFileDetailsForm = Object.assign(this.baseSendFileDetailsForm, this.sendDetailsForm);
+
+ this.sendFormContainer.registerChildForm("sendFileDetailsForm", this.sendFileDetailsForm);
+
+ this.sendFileDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
+ this.sendFormContainer.patchSend((send) => {
+ return Object.assign(send, {
+ file: value.file,
+ });
+ });
+ });
+ }
+
+ onFileSelected = (event: Event): void => {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (!file) {
+ return;
+ }
+ this.fileName = file.name;
+ this.sendFormContainer.onFileSelected(file);
+ };
+
+ ngOnInit() {
+ if (this.originalSendView) {
+ this.sendFileDetailsForm.patchValue({
+ file: this.originalSendView.file,
+ });
+ }
+ }
+}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts
index 2cb1907d921..b265b644df4 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts
+++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts
@@ -65,6 +65,7 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
private bitSubmit: BitSubmitDirective;
private destroyRef = inject(DestroyRef);
private _firstInitialized = false;
+ private file: File | null = null;
/**
* The form ID to use for the form. Used to connect it to a submit button.
@@ -188,14 +189,17 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
private i18nService: I18nService,
) {}
+ onFileSelected(file: File): void {
+ this.file = file;
+ }
+
submit = async () => {
if (this.sendForm.invalid) {
this.sendForm.markAllAsTouched();
return;
}
- // TODO: Add file handling
- await this.addEditFormService.saveSend(this.updatedSendView, null, this.config);
+ await this.addEditFormService.saveSend(this.updatedSendView, this.file, this.config);
this.toastService.showToast({
variant: "success",
diff --git a/libs/tools/send/send-ui/src/send-form/send-form-container.ts b/libs/tools/send/send-ui/src/send-form/send-form-container.ts
index f3af1ecd816..21508d96727 100644
--- a/libs/tools/send/send-ui/src/send-form/send-form-container.ts
+++ b/libs/tools/send/send-ui/src/send-form/send-form-container.ts
@@ -2,6 +2,7 @@ import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendFormConfig } from "./abstractions/send-form-config.service";
import { SendDetailsComponent } from "./components/send-details/send-details.component";
+import { SendFileDetailsForm } from "./components/send-details/send-file-details.component";
import { SendTextDetailsForm } from "./components/send-details/send-text-details.component";
/**
* The complete form for a send. Includes all the sub-forms from their respective section components.
@@ -10,6 +11,7 @@ import { SendTextDetailsForm } from "./components/send-details/send-text-details
export type SendForm = {
sendDetailsForm?: SendDetailsComponent["sendDetailsForm"];
sendTextDetailsForm?: SendTextDetailsForm;
+ sendFileDetailsForm?: SendFileDetailsForm;
};
/**
@@ -37,5 +39,7 @@ export abstract class SendFormContainer {
group: Exclude,
): void;
+ abstract onFileSelected(file: File): void;
+
abstract patchSend(updateFn: (current: SendView) => SendView): void;
}
From a96e3cb64729bf7e850f80e27aae3ef27ff1e4dc Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 18 Sep 2024 23:41:07 +0200
Subject: [PATCH 048/104] [deps] Tools: Update electron to v32.1.1 (#11067)
* [deps] Tools: Update electron to v32.1.1
* Bump version in electron-builder.json
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith
---
apps/desktop/electron-builder.json | 2 +-
package-lock.json | 8 ++++----
package.json | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json
index 49e414dfe99..09783f26f49 100644
--- a/apps/desktop/electron-builder.json
+++ b/apps/desktop/electron-builder.json
@@ -25,7 +25,7 @@
"**/node_modules/argon2/package.json",
"**/node_modules/argon2/build/Release/argon2.node"
],
- "electronVersion": "32.0.2",
+ "electronVersion": "32.1.1",
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",
diff --git a/package-lock.json b/package-lock.json
index e71763cde27..95e8fcb50b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -129,7 +129,7 @@
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
- "electron": "32.0.2",
+ "electron": "32.1.1",
"electron-builder": "24.13.3",
"electron-log": "5.0.1",
"electron-reload": "2.0.0-alpha.1",
@@ -16414,9 +16414,9 @@
}
},
"node_modules/electron": {
- "version": "32.0.2",
- "resolved": "https://registry.npmjs.org/electron/-/electron-32.0.2.tgz",
- "integrity": "sha512-nmZblq8wW3HZ17MAyaUuiMI9Mb0Cgc7UR3To85h/rVopbfyF5s34NxtK4gvyRfYPxpDGP4k+HoQIPniPPrdE3w==",
+ "version": "32.1.1",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-32.1.1.tgz",
+ "integrity": "sha512-NlWvG6kXOJbZbELmzP3oV7u50I3NHYbCeh+AkUQ9vGyP7b74cFMx9HdTzejODeztW1jhr3SjIBbUZzZ45zflfQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
diff --git a/package.json b/package.json
index 8e5f38c62ef..aafc92bbd3d 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
- "electron": "32.0.2",
+ "electron": "32.1.1",
"electron-builder": "24.13.3",
"electron-log": "5.0.1",
"electron-reload": "2.0.0-alpha.1",
From 64844600dcdafea0b155d3375b3d768aeb467057 Mon Sep 17 00:00:00 2001
From: Vince Grassia <593223+vgrassia@users.noreply.github.com>
Date: Thu, 19 Sep 2024 04:40:55 -0400
Subject: [PATCH 049/104] Fix alert message for Desktop builds (#11139)
---
.github/workflows/build-desktop.yml | 20 +++-----------------
1 file changed, 3 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index 8170f1eef00..ddb87320839 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -1037,11 +1037,7 @@ jobs:
--type macos \
--file "$(find ./dist/mas-universal/Bitwarden*.pkg)" \
--apiKey $APP_STORE_CONNECT_AUTH_KEY \
- --apiIssuer $APP_STORE_CONNECT_TEAM_ISSUER \
- &> output.txt
-
- UUID=$(cat output.txt | grep "Delivery UUID" | sed -E 's/Delivery UUID: (.*)/\1/')
- echo "uuid=$UUID" >> $GITHUB_OUTPUT
+ --apiIssuer $APP_STORE_CONNECT_TEAM_ISSUER
- name: Post message to a Slack channel
id: slack-message
@@ -1059,24 +1055,14 @@ jobs:
"type": "section",
"text": {
"type": "mrkdwn",
- "text": "Desktop client v${{ env._PACKAGE_VERSION }} <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|build> success on *${{ github.ref_name }}*"
- },
- "accessory": {
- "type": "button",
- "text": {
- "type": "plain_text",
- "text": "TestFlight Build",
- "emoji": true
- },
- "url": "https://appstoreconnect.apple.com/teams/${{ env.APP_STORE_CONNECT_TEAM_ISSUER }}/apps/1352778147/testflight/macos/${{ env.BUILD_UUID }}"
+ "text": "Desktop client v${{ env._PACKAGE_VERSION }} <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|build ${{ env.BUILD_NUMBER }}> success on *${{ github.ref_name }}*"
}
}
]
}
env:
- APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
SLACK_BOT_TOKEN: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }}
- BUILD_UUID: ${{ steps.testflight-deploy.outputs.uuid }}
+ BUILD_NUMBER: ${{ needs.setup.outputs.build_number }}
macos-package-dev:
From 7f9c5cedaf344824cfd50291312c90376e83725d Mon Sep 17 00:00:00 2001
From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Date: Thu, 19 Sep 2024 14:34:53 +0200
Subject: [PATCH 050/104] Delete SendForm storybook stories (#11149)
Co-authored-by: Daniel James Smith
---
.../send/send-ui/src/send-form/send-form.mdx | 17 ---
.../src/send-form/send-form.stories.ts | 134 ------------------
2 files changed, 151 deletions(-)
delete mode 100644 libs/tools/send/send-ui/src/send-form/send-form.mdx
delete mode 100644 libs/tools/send/send-ui/src/send-form/send-form.stories.ts
diff --git a/libs/tools/send/send-ui/src/send-form/send-form.mdx b/libs/tools/send/send-ui/src/send-form/send-form.mdx
deleted file mode 100644
index d1297ee90ca..00000000000
--- a/libs/tools/send/send-ui/src/send-form/send-form.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Controls, Meta, Primary } from "@storybook/addon-docs";
-
-import * as stories from "./send-form.stories";
-
-
-
-# Send Form
-
-The send form is a re-usable form component that can be used to create, update, and clone sends. It
-is configured via a `SendFormConfig` object that is passed to the component as a prop. The
-`SendFormConfig` object can be created manually, or a `SendFormConfigService` can be used to create
-it. A default implementation of the `SendFormConfigService` exists in the `@bitwarden/send-ui`
-library.
-
-
-
-
diff --git a/libs/tools/send/send-ui/src/send-form/send-form.stories.ts b/libs/tools/send/send-ui/src/send-form/send-form.stories.ts
deleted file mode 100644
index 2c47bd62626..00000000000
--- a/libs/tools/send/send-ui/src/send-form/send-form.stories.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { importProvidersFrom } from "@angular/core";
-import { action } from "@storybook/addon-actions";
-import {
- applicationConfig,
- componentWrapperDecorator,
- Meta,
- moduleMetadata,
- StoryObj,
-} from "@storybook/angular";
-
-import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
-import { Send } from "@bitwarden/common/tools/send/models/domain/send";
-import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
-import { AsyncActionsModule, ButtonModule, ToastService } from "@bitwarden/components";
-import { SendFormConfig } from "@bitwarden/send-ui";
-// FIXME: remove `/apps` import from `/libs`
-// eslint-disable-next-line import/no-restricted-paths
-import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests";
-
-import { SendFormService } from "./abstractions/send-form.service";
-import { SendFormComponent } from "./components/send-form.component";
-import { SendFormModule } from "./send-form.module";
-
-const defaultConfig: SendFormConfig = {
- mode: "add",
- sendType: SendType.Text,
- areSendsAllowed: true,
- originalSend: {
- id: "123",
- name: "Test Send",
- notes: "Example notes",
- } as unknown as Send,
-};
-
-class TestAddEditFormService implements SendFormService {
- decryptSend(): Promise {
- return Promise.resolve(defaultConfig.originalSend as any);
- }
- async saveSend(send: SendView, file: File | ArrayBuffer): Promise {
- await new Promise((resolve) => setTimeout(resolve, 1000));
- return send;
- }
-}
-
-const actionsData = {
- onSave: action("onSave"),
-};
-
-export default {
- title: "Tools/Send Form",
- component: SendFormComponent,
- decorators: [
- moduleMetadata({
- imports: [SendFormModule, AsyncActionsModule, ButtonModule],
- providers: [
- {
- provide: SendFormService,
- useClass: TestAddEditFormService,
- },
- {
- provide: ToastService,
- useValue: {
- showToast: action("showToast"),
- },
- },
- ],
- }),
- componentWrapperDecorator(
- (story) => `${story} `,
- ),
- applicationConfig({
- providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
- }),
- ],
- args: {
- config: defaultConfig,
- },
- argTypes: {
- config: {
- description: "The configuration object for the form.",
- },
- },
-} as Meta;
-
-type Story = StoryObj;
-
-export const Default: Story = {
- render: (args) => {
- return {
- props: {
- onSave: actionsData.onSave,
- ...args,
- },
- template: /*html*/ `
-
-
- `,
- };
- },
-};
-
-export const Edit: Story = {
- ...Default,
- args: {
- config: {
- ...defaultConfig,
- mode: "edit",
- originalSend: defaultConfig.originalSend,
- },
- },
-};
-
-export const PartialEdit: Story = {
- ...Default,
- args: {
- config: {
- ...defaultConfig,
- mode: "partial-edit",
- originalSend: defaultConfig.originalSend,
- },
- },
-};
-
-export const SendsHaveBeenDisabledByPolicy: Story = {
- ...Default,
- args: {
- config: {
- ...defaultConfig,
- mode: "add",
- areSendsAllowed: false,
- originalSend: defaultConfig.originalSend,
- },
- },
-};
From 354079725ffdd2c0379b78a26b801ff40c4a0121 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Anders=20=C3=85berg?=
Date: Thu, 19 Sep 2024 14:45:45 +0200
Subject: [PATCH 051/104] PM-7673: Reduce syncs when signing in with passkeys
(#10817)
* Reduce syncs when signing in with passkeys
* PM-7673: Reduce syncs when creating a passkey (#10824)
* Reduce to syncs when creating a passkey
* Mocked rxjs stream
---
.../fido2/fido2-authenticator.service.spec.ts | 6 ++-
.../fido2/fido2-authenticator.service.ts | 42 +++++++++++++++----
2 files changed, 37 insertions(+), 11 deletions(-)
diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts
index 289e94e6eff..bd9bb6e5f6f 100644
--- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts
+++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts
@@ -1,7 +1,7 @@
import { TextEncoder } from "util";
import { mock, MockProxy } from "jest-mock-extended";
-import { BehaviorSubject } from "rxjs";
+import { BehaviorSubject, of } from "rxjs";
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
@@ -53,7 +53,9 @@ describe("FidoAuthenticatorService", () => {
userInterface = mock();
userInterfaceSession = mock();
userInterface.newSession.mockResolvedValue(userInterfaceSession);
- syncService = mock();
+ syncService = mock({
+ activeUserLastSync$: () => of(new Date()),
+ });
accountService = mock();
authenticator = new Fido2AuthenticatorService(
cipherService,
diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts
index ddcc079eb95..8f0523769d9 100644
--- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts
+++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts
@@ -94,7 +94,14 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
await userInterfaceSession.ensureUnlockedVault();
- await this.syncService.fullSync(false);
+
+ // Avoid syncing if we did it reasonably soon as the only reason for syncing is to validate excludeCredentials
+ const lastSync = await firstValueFrom(this.syncService.activeUserLastSync$());
+ const threshold = new Date().getTime() - 1000 * 60 * 30; // 30 minutes ago
+
+ if (!lastSync || lastSync.getTime() < threshold) {
+ await this.syncService.fullSync(false);
+ }
const existingCipherIds = await this.findExcludedCredentials(
params.excludeCredentialDescriptorList,
@@ -223,15 +230,17 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
let cipherOptions: CipherView[];
await userInterfaceSession.ensureUnlockedVault();
- await this.syncService.fullSync(false);
- if (params.allowCredentialDescriptorList?.length > 0) {
- cipherOptions = await this.findCredentialsById(
- params.allowCredentialDescriptorList,
- params.rpId,
- );
- } else {
- cipherOptions = await this.findCredentialsByRp(params.rpId);
+ // Try to find the passkey locally before causing a sync to speed things up
+ // only skip syncing if we found credentials AND all of them have a counter = 0
+ cipherOptions = await this.findCredential(params, cipherOptions);
+ if (
+ cipherOptions.length === 0 ||
+ cipherOptions.some((c) => c.login.fido2Credentials.some((p) => p.counter > 0))
+ ) {
+ // If no passkey is found, or any had a non-zero counter, sync to get the latest data
+ await this.syncService.fullSync(false);
+ cipherOptions = await this.findCredential(params, cipherOptions);
}
if (cipherOptions.length === 0) {
@@ -335,6 +344,21 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
}
+ private async findCredential(
+ params: Fido2AuthenticatorGetAssertionParams,
+ cipherOptions: CipherView[],
+ ) {
+ if (params.allowCredentialDescriptorList?.length > 0) {
+ cipherOptions = await this.findCredentialsById(
+ params.allowCredentialDescriptorList,
+ params.rpId,
+ );
+ } else {
+ cipherOptions = await this.findCredentialsByRp(params.rpId);
+ }
+ return cipherOptions;
+ }
+
private requiresUserVerificationPrompt(
params: Fido2AuthenticatorGetAssertionParams,
cipherOptions: CipherView[],
From 6ffd85c42cf0df1784e1cc80181260fedc1ecf49 Mon Sep 17 00:00:00 2001
From: Oscar Hinton
Date: Thu, 19 Sep 2024 14:56:47 +0200
Subject: [PATCH 052/104] [PM-11342] Fix 404 missing styles (#11147)
Fix 404 page lacking styles on nested pages.
---
apps/web/webpack.config.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js
index cec4bf044be..99dc7479257 100644
--- a/apps/web/webpack.config.js
+++ b/apps/web/webpack.config.js
@@ -135,6 +135,8 @@ const plugins = [
template: "./src/404.html",
filename: "404.html",
chunks: ["styles"],
+ // 404 page is a wildcard, this ensures it uses absolute paths.
+ publicPath: "/",
}),
new CopyWebpackPlugin({
patterns: [
From e5bb2e58a63f722dfb9dc3087c6d5548883e05f8 Mon Sep 17 00:00:00 2001
From: Oscar Hinton
Date: Thu, 19 Sep 2024 15:00:07 +0200
Subject: [PATCH 053/104] [PM-12004] Fix ~60 compile warnings in webpack for
web (#11004)
---
apps/web/tsconfig.build.json | 5 +++++
apps/web/webpack.config.js | 2 +-
bitwarden_license/bit-web/tsconfig.build.json | 14 ++++++++++++++
bitwarden_license/bit-web/webpack.config.js | 2 +-
4 files changed, 21 insertions(+), 2 deletions(-)
create mode 100644 apps/web/tsconfig.build.json
create mode 100644 bitwarden_license/bit-web/tsconfig.build.json
diff --git a/apps/web/tsconfig.build.json b/apps/web/tsconfig.build.json
new file mode 100644
index 00000000000..b10c4f9d899
--- /dev/null
+++ b/apps/web/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.json",
+ "files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"],
+ "include": ["src/connectors/*.ts", "../../libs/common/src/platform/services/**/*.worker.ts"]
+}
diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js
index 99dc7479257..fee60264ea4 100644
--- a/apps/web/webpack.config.js
+++ b/apps/web/webpack.config.js
@@ -178,7 +178,7 @@ const plugins = [
ADDITIONAL_REGIONS: envConfig["additionalRegions"] ?? [],
}),
new AngularWebpackPlugin({
- tsConfigPath: "tsconfig.json",
+ tsconfig: "tsconfig.build.json",
entryModule: "src/app/app.module#AppModule",
sourceMap: true,
}),
diff --git a/bitwarden_license/bit-web/tsconfig.build.json b/bitwarden_license/bit-web/tsconfig.build.json
new file mode 100644
index 00000000000..9bebbeb5061
--- /dev/null
+++ b/bitwarden_license/bit-web/tsconfig.build.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./tsconfig",
+ "files": [
+ "../../apps/web/src/polyfills.ts",
+ "../../apps/web/src/main.ts",
+ "../../apps/web/src/theme.ts",
+
+ "../../bitwarden_license/bit-web/src/main.ts"
+ ],
+ "include": [
+ "../../apps/web/src/connectors/*.ts",
+ "../../libs/common/src/platform/services/**/*.worker.ts"
+ ]
+}
diff --git a/bitwarden_license/bit-web/webpack.config.js b/bitwarden_license/bit-web/webpack.config.js
index bf192b5411a..ce5f0075afc 100644
--- a/bitwarden_license/bit-web/webpack.config.js
+++ b/bitwarden_license/bit-web/webpack.config.js
@@ -4,7 +4,7 @@ const webpackConfig = require("../../apps/web/webpack.config");
webpackConfig.entry["app/main"] = "../../bitwarden_license/bit-web/src/main.ts";
webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({
- tsconfig: "../../bitwarden_license/bit-web/tsconfig.json",
+ tsconfig: "../../bitwarden_license/bit-web/tsconfig.build.json",
entryModule: "bitwarden_license/src/app/app.module#AppModule",
sourceMap: true,
});
From 19f4afcd2bc9419e053ee4cc85c52771485bf730 Mon Sep 17 00:00:00 2001
From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com>
Date: Thu, 19 Sep 2024 14:27:51 +0000
Subject: [PATCH 054/104] Bumped client version(s) (#11152)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
---
apps/desktop/package.json | 2 +-
apps/desktop/src/package-lock.json | 4 ++--
apps/desktop/src/package.json | 2 +-
package-lock.json | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 4562653978b..40ce2fec5d9 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
- "version": "2024.9.0",
+ "version": "2024.9.1",
"keywords": [
"bitwarden",
"password",
diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json
index 6823bddceb8..ce08dfde2cd 100644
--- a/apps/desktop/src/package-lock.json
+++ b/apps/desktop/src/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
- "version": "2024.9.0",
+ "version": "2024.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
- "version": "2024.9.0",
+ "version": "2024.9.1",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi",
diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json
index 18a046d5bce..e34641f1e09 100644
--- a/apps/desktop/src/package.json
+++ b/apps/desktop/src/package.json
@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
- "version": "2024.9.0",
+ "version": "2024.9.1",
"author": "Bitwarden Inc. (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
diff --git a/package-lock.json b/package-lock.json
index 95e8fcb50b4..bfc943b55b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -232,7 +232,7 @@
},
"apps/desktop": {
"name": "@bitwarden/desktop",
- "version": "2024.9.0",
+ "version": "2024.9.1",
"hasInstallScript": true,
"license": "GPL-3.0"
},
From afff91e0f378e27df417c3b5a3b9e9da5254c0e7 Mon Sep 17 00:00:00 2001
From: Ike <137194738+ike-kottlowski@users.noreply.github.com>
Date: Thu, 19 Sep 2024 11:32:42 -0400
Subject: [PATCH 055/104] added duofederal.com to valid urls (#11137)
---
apps/web/src/connectors/duo-redirect.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts
index 84551258610..ddbf75e53ca 100644
--- a/apps/web/src/connectors/duo-redirect.ts
+++ b/apps/web/src/connectors/duo-redirect.ts
@@ -52,7 +52,11 @@ window.addEventListener("load", async () => {
function redirectToDuoFrameless(redirectUrl: string) {
const validateUrl = new URL(redirectUrl);
- if (validateUrl.protocol !== "https:" || !validateUrl.hostname.endsWith("duosecurity.com")) {
+ if (
+ validateUrl.protocol !== "https:" ||
+ !validateUrl.hostname.endsWith("duosecurity.com") ||
+ !validateUrl.hostname.endsWith("duofederal.com")
+ ) {
throw new Error("Invalid redirect URL");
}
From 01e530d02bfd253afacc01e9c5622e0e052e4e0f Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Thu, 19 Sep 2024 10:55:40 -0500
Subject: [PATCH 056/104] [PM-11691] Remove Nord and Solarized Dark from
extension (#11013)
* remove nord and solarized dark from AppearanceV2 component
- This component already behind the extension refresh feature flag
* update the users theme to system when nord or solarized dark is selected
* For desktop, still allow all theme types by overriding the default theme service.
* change theme on the fly rather than updating local state.
- When the feature flag is removed then a migration will have to take place
---
.../browser/src/background/main.background.ts | 7 ++++--
.../popup/settings/appearance-v2.component.ts | 2 --
.../src/app/services/desktop-theme.service.ts | 25 +++++++++++++++++++
.../src/app/services/services.module.ts | 7 ++++++
apps/web/src/app/core/core.module.ts | 7 +++---
.../src/services/jslib-services.module.ts | 2 +-
.../platform/theming/theme-state.service.ts | 23 ++++++++++++++---
7 files changed, 62 insertions(+), 11 deletions(-)
create mode 100644 apps/desktop/src/app/services/desktop-theme.service.ts
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index f54f1de1dd5..62d83b19009 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -610,8 +610,6 @@ export default class MainBackground {
migrationRunner,
);
- this.themeStateService = new DefaultThemeStateService(this.globalStateProvider);
-
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
@@ -785,6 +783,11 @@ export default class MainBackground {
this.authService,
);
+ this.themeStateService = new DefaultThemeStateService(
+ this.globalStateProvider,
+ this.configService,
+ );
+
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
this.cipherService = new CipherService(
diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts
index 7ca073d51b0..12f5c540409 100644
--- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts
+++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts
@@ -59,8 +59,6 @@ export class AppearanceV2Component implements OnInit {
{ name: i18nService.t("systemDefault"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
- { name: "Nord", value: ThemeType.Nord },
- { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
}
diff --git a/apps/desktop/src/app/services/desktop-theme.service.ts b/apps/desktop/src/app/services/desktop-theme.service.ts
new file mode 100644
index 00000000000..321aff677df
--- /dev/null
+++ b/apps/desktop/src/app/services/desktop-theme.service.ts
@@ -0,0 +1,25 @@
+import { map } from "rxjs";
+
+import { ThemeType } from "@bitwarden/common/platform/enums";
+import { GlobalStateProvider } from "@bitwarden/common/platform/state";
+import {
+ THEME_SELECTION,
+ ThemeStateService,
+} from "@bitwarden/common/platform/theming/theme-state.service";
+
+export class DesktopThemeStateService implements ThemeStateService {
+ private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
+
+ selectedTheme$ = this.selectedThemeState.state$.pipe(map((theme) => theme ?? this.defaultTheme));
+
+ constructor(
+ private globalStateProvider: GlobalStateProvider,
+ private defaultTheme: ThemeType = ThemeType.System,
+ ) {}
+
+ async setSelectedTheme(theme: ThemeType): Promise {
+ await this.selectedThemeState.update(() => theme, {
+ shouldUpdate: (currentTheme) => currentTheme !== theme,
+ });
+ }
+}
diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts
index d4b51ca1c7e..d5672f54c0f 100644
--- a/apps/desktop/src/app/services/services.module.ts
+++ b/apps/desktop/src/app/services/services.module.ts
@@ -66,6 +66,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
+import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components";
@@ -93,6 +94,7 @@ import { SearchBarService } from "../layout/search/search-bar.service";
import { DesktopFileDownloadService } from "./desktop-file-download.service";
import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service";
+import { DesktopThemeStateService } from "./desktop-theme.service";
import { InitService } from "./init.service";
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
@@ -212,6 +214,11 @@ const safeProviders: SafeProvider[] = [
useFactory: () => fromIpcSystemTheme(),
deps: [],
}),
+ safeProvider({
+ provide: ThemeStateService,
+ useClass: DesktopThemeStateService,
+ deps: [GlobalStateProvider],
+ }),
safeProvider({
provide: EncryptedMessageHandlerService,
deps: [
diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts
index 887c8fb626a..5bf9373b032 100644
--- a/apps/web/src/app/core/core.module.ts
+++ b/apps/web/src/app/core/core.module.ts
@@ -33,6 +33,7 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@@ -174,10 +175,10 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: ThemeStateService,
- useFactory: (globalStateProvider: GlobalStateProvider) =>
+ useFactory: (globalStateProvider: GlobalStateProvider, configService: ConfigService) =>
// Web chooses to have Light as the default theme
- new DefaultThemeStateService(globalStateProvider, ThemeType.Light),
- deps: [GlobalStateProvider],
+ new DefaultThemeStateService(globalStateProvider, configService, ThemeType.Light),
+ deps: [GlobalStateProvider, ConfigService],
}),
safeProvider({
provide: CLIENT_TYPE,
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index cdf6a273909..4cdf5be8651 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -364,7 +364,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: ThemeStateService,
useClass: DefaultThemeStateService,
- deps: [GlobalStateProvider],
+ deps: [GlobalStateProvider, ConfigService],
}),
safeProvider({
provide: AbstractThemingService,
diff --git a/libs/common/src/platform/theming/theme-state.service.ts b/libs/common/src/platform/theming/theme-state.service.ts
index 9c31733416b..bb146be4927 100644
--- a/libs/common/src/platform/theming/theme-state.service.ts
+++ b/libs/common/src/platform/theming/theme-state.service.ts
@@ -1,5 +1,7 @@
-import { Observable, map } from "rxjs";
+import { Observable, combineLatest, map } from "rxjs";
+import { FeatureFlag } from "../../enums/feature-flag.enum";
+import { ConfigService } from "../abstractions/config/config.service";
import { ThemeType } from "../enums";
import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "../state";
@@ -16,17 +18,32 @@ export abstract class ThemeStateService {
abstract setSelectedTheme(theme: ThemeType): Promise;
}
-const THEME_SELECTION = new KeyDefinition(THEMING_DISK, "selection", {
+export const THEME_SELECTION = new KeyDefinition(THEMING_DISK, "selection", {
deserializer: (s) => s,
});
export class DefaultThemeStateService implements ThemeStateService {
private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
- selectedTheme$ = this.selectedThemeState.state$.pipe(map((theme) => theme ?? this.defaultTheme));
+ selectedTheme$ = combineLatest([
+ this.selectedThemeState.state$,
+ this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
+ ]).pipe(
+ map(([theme, isExtensionRefresh]) => {
+ // The extension refresh should not allow for Nord or SolarizedDark
+ // Default the user to their system theme
+ if (isExtensionRefresh && [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme)) {
+ return ThemeType.System;
+ }
+
+ return theme;
+ }),
+ map((theme) => theme ?? this.defaultTheme),
+ );
constructor(
private globalStateProvider: GlobalStateProvider,
+ private configService: ConfigService,
private defaultTheme: ThemeType = ThemeType.System,
) {}
From 7ae427e713733f43f4d916b1208aa990e39fd54c Mon Sep 17 00:00:00 2001
From: SmithThe4th
Date: Thu, 19 Sep 2024 13:27:08 -0400
Subject: [PATCH 057/104] Revert "[PM-11200] Move delete item permission to Can
Manage (#10890)" (#11155)
* Revert "[PM-11200] Move delete item permission to Can Manage (#10890)"
This reverts commit 8921230b4f5f33d0948572e47261604a6a3d731e.
* Removed provider access
---
.../vault-cipher-row.component.html | 2 +-
.../vault-items/vault-cipher-row.component.ts | 1 -
.../vault-items/vault-items.component.html | 3 --
.../vault-items/vault-items.component.ts | 39 ++++---------------
.../individual-vault/vault.component.html | 1 -
5 files changed, 8 insertions(+), 38 deletions(-)
diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html
index 6a04ff6071d..524d9dff20b 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html
+++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html
@@ -157,7 +157,7 @@
{{ "loggingInAs" | i18n }} {{ loggedEmail }}
- {{ "notYou" | i18n }}
+ {{ "notYou" | i18n }}
diff --git a/apps/web/src/app/auth/register-form/register-form.component.html b/apps/web/src/app/auth/register-form/register-form.component.html
index 2e50d69fce1..19a7a95b298 100644
--- a/apps/web/src/app/auth/register-form/register-form.component.html
+++ b/apps/web/src/app/auth/register-form/register-form.component.html
@@ -115,11 +115,11 @@
{{ "acceptPolicies" | i18n }}
- {{
+ {{
"termsOfService" | i18n
}},
- {{
+ {{
"privacyPolicy" | i18n
}}
@@ -151,7 +151,7 @@
{{ "alreadyHaveAccount" | i18n }}
- {{ "logIn" | i18n }}
+ {{ "logIn" | i18n }}
diff --git a/apps/web/src/app/auth/settings/change-password.component.html b/apps/web/src/app/auth/settings/change-password.component.html
index b5c8677405f..91144fdfc1f 100644
--- a/apps/web/src/app/auth/settings/change-password.component.html
+++ b/apps/web/src/app/auth/settings/change-password.component.html
@@ -104,7 +104,7 @@
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
target="_blank"
rel="noreferrer"
- appA11yTitle="{{ 'learnMore' | i18n }}"
+ appA11yTitle="{{ 'impactOfRotatingYourEncryptionKey' | i18n }}"
>
diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html
index 293c5051ce9..6e87d66d18b 100644
--- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html
+++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html
@@ -21,7 +21,7 @@
rel="noreferrer"
bitLink
linkType="primary"
- appA11yTitle="{{ 'learnMore' | i18n }}"
+ appA11yTitle="{{ 'learnMoreAboutUserAccess' | i18n }}"
href="https://bitwarden.com/help/emergency-access/#user-access"
slot="end"
>
diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html
index e38f21fde20..fc199a55a76 100644
--- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html
+++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html
@@ -9,7 +9,7 @@
target="_blank"
rel="noreferrer"
>
- {{ "learnMore" | i18n }}.
+ {{ "learnMoreAboutEmergencyAccess" | i18n }}
{{
diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html
index 478cd77eb6c..203f6d016bb 100644
--- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html
+++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html
@@ -21,7 +21,7 @@
href="https://bitwarden.com/help/kdf-algorithms"
target="_blank"
rel="noreferrer"
- appA11yTitle="{{ 'learnMore' | i18n }}"
+ appA11yTitle="{{ 'learnMoreAboutEncryptionAlgorithms' | i18n }}"
slot="end"
>
@@ -57,7 +57,7 @@
href="https://bitwarden.com/help/what-encryption-is-used/#changing-kdf-iterations"
target="_blank"
rel="noreferrer"
- appA11yTitle="{{ 'learnMore' | i18n }}"
+ appA11yTitle="{{ 'learnMoreAboutKDFIterations' | i18n }}"
slot="end"
>
diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor-setup.component.html
index 3595d9a7dcb..b7cd6954fd6 100644
--- a/apps/web/src/app/auth/settings/two-factor-setup.component.html
+++ b/apps/web/src/app/auth/settings/two-factor-setup.component.html
@@ -11,7 +11,7 @@
{{ "twoStepLoginEnterpriseDescStart" | i18n }}
- {{ "twoStepLoginPolicy" | i18n }}.
+ {{ "twoStepLoginPolicy" | i18n }}.
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor-webauthn.component.html
index 9dc9bd40684..0a2eb346b1b 100644
--- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.html
+++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.html
@@ -42,7 +42,7 @@
aria-hidden="true"
>
-
- {{ "remove" | i18n }}
+ {{ "remove" | i18n }}
diff --git a/apps/web/src/app/billing/shared/billing-history.component.html b/apps/web/src/app/billing/shared/billing-history.component.html
index 1719a59076f..fc621571778 100644
--- a/apps/web/src/app/billing/shared/billing-history.component.html
+++ b/apps/web/src/app/billing/shared/billing-history.component.html
@@ -15,7 +15,13 @@
>
-
+
{{ "invoiceNumber" | i18n: i.number }}
|
diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html
index f33768c2c95..d4853713579 100644
--- a/apps/web/src/app/billing/shared/payment/payment.component.html
+++ b/apps/web/src/app/billing/shared/payment/payment.component.html
@@ -55,7 +55,7 @@
target="_blank"
rel="noreferrer"
class="hover:tw-no-underline"
- appA11yTitle="{{ 'learnMore' | i18n }}"
+ appA11yTitle="{{ 'whatIsACvvNumber' | i18n }}"
>