-
-
![]()
-
-
-
{{ a.title }}
-
-
{{ "compromisedData" | i18n }}:
-
-
-
-
- - {{ "website" | i18n }}
- - {{ a.domain }}
- - {{ "affectedUsers" | i18n }}
- - {{ a.pwnCount | number }}
- - {{ "breachOccurred" | i18n }}
- - {{ a.breachDate | date: "mediumDate" }}
- - {{ "breachReported" | i18n }}
- - {{ a.addedDate | date: "mediumDate" }}
-
-
+
+ -
+
+
![]()
+
+
+
{{ a.title }}
+
+
{{ "compromisedData" | i18n }}:
+
+
+
+
+ - {{ "website" | i18n }}
+ - {{ a.domain }}
+ - {{ "affectedUsers" | i18n }}
+ - {{ a.pwnCount | number }}
+ - {{ "breachOccurred" | i18n }}
+ - {{ a.breachDate | date: "mediumDate" }}
+ - {{ "breachReported" | i18n }}
+ - {{ a.addedDate | date: "mediumDate" }}
+
diff --git a/apps/web/src/images/search.svg b/apps/web/src/images/search.svg
new file mode 100644
index 00000000000..36e0ea4bd23
--- /dev/null
+++ b/apps/web/src/images/search.svg
@@ -0,0 +1,12 @@
+
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 85a7b8cb927..3d6cf0f23a5 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -6306,6 +6306,21 @@
"sponsoredFamilies": {
"message": "Free Bitwarden Families"
},
+ "sponsoredBitwardenFamilies": {
+ "message": "Sponsored families"
+ },
+ "noSponsoredFamilies": {
+ "message": "No sponsored families"
+ },
+ "noSponsoredFamiliesDescription": {
+ "message": "Sponsored non-member families plans will display here"
+ },
+ "sponsorFreeBitwardenFamilies": {
+ "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization."
+ },
+ "sponsoredFamiliesRemoveActiveSponsorship": {
+ "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization."
+ },
"sponsoredFamiliesEligible": {
"message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work."
},
@@ -6321,6 +6336,18 @@
"sponsoredFamiliesSharedCollections": {
"message": "Shared collections for Family secrets"
},
+ "memberFamilies": {
+ "message": "Member families"
+ },
+ "noMemberFamilies": {
+ "message": "No member families"
+ },
+ "noMemberFamiliesDescription": {
+ "message": "Members who have redeemed family plans will display here"
+ },
+ "membersWithSponsoredFamilies": {
+ "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization."
+ },
"badToken": {
"message": "The link is no longer valid. Please have the sponsor resend the offer."
},
@@ -7984,6 +8011,9 @@
"inviteMember": {
"message": "Invite member"
},
+ "addSponsorship": {
+ "message": "Add sponsorship"
+ },
"needsConfirmation": {
"message": "Needs confirmation"
},
diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts
index 8a198663e06..4ca18b4985e 100644
--- a/libs/auth/src/angular/login/login.component.ts
+++ b/libs/auth/src/angular/login/login.component.ts
@@ -536,6 +536,10 @@ export class LoginComponent implements OnInit, OnDestroy {
if (storedEmail) {
this.formGroup.controls.email.setValue(storedEmail);
this.formGroup.controls.rememberEmail.setValue(true);
+ // If we load an email into the form, we need to initialize it for the login process as well
+ // so that other login components can use it.
+ // We do this here as it's possible that a user doesn't edit the email field before submitting.
+ this.loginEmailService.setLoginEmail(storedEmail);
} else {
this.formGroup.controls.rememberEmail.setValue(false);
}
diff --git a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts
index 534afffd1bb..19e993487c2 100644
--- a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts
+++ b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts
@@ -6,4 +6,5 @@ export class OrganizationSponsorshipCreateRequest {
sponsoredEmail: string;
planSponsorshipType: PlanSponsorshipType;
friendlyName: string;
+ notes?: string;
}
diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts
index 40d8db03d3b..bfeecb4eb23 100644
--- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts
+++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts
@@ -1,4 +1,4 @@
-import { PlanType } from "../../enums";
+import { PlanSponsorshipType, PlanType } from "../../enums";
export class PreviewOrganizationInvoiceRequest {
organizationId?: string;
@@ -21,6 +21,7 @@ export class PreviewOrganizationInvoiceRequest {
class PasswordManager {
plan: PlanType;
+ sponsoredPlan?: PlanSponsorshipType;
seats: number;
additionalStorage: number;
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 8850c18c051..c3c4761fa05 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -46,7 +46,6 @@ export enum FeatureFlag {
CriticalApps = "pm-14466-risk-insights-critical-application",
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
DesktopSendUIRefresh = "desktop-send-ui-refresh",
- ExportAttachments = "export-attachments",
/* Vault */
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
@@ -98,7 +97,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.CriticalApps]: FALSE,
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
- [FeatureFlag.ExportAttachments]: FALSE,
/* Vault */
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts
index 69f77c6ca32..4e9b4175838 100644
--- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts
+++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts
@@ -39,8 +39,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EventType } from "@bitwarden/common/enums";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -184,10 +182,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
private onlyManagedCollections = true;
private onGenerate$ = new Subject
();
- private isExportAttachmentsEnabled$ = this.configService.getFeatureFlag$(
- FeatureFlag.ExportAttachments,
- );
-
constructor(
protected i18nService: I18nService,
protected toastService: ToastService,
@@ -202,7 +196,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
protected organizationService: OrganizationService,
private accountService: AccountService,
private collectionService: CollectionService,
- private configService: ConfigService,
) {}
async ngOnInit() {
@@ -225,17 +218,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
),
);
- combineLatest([
- this.exportForm.controls.vaultSelector.valueChanges,
- this.isExportAttachmentsEnabled$,
- ])
+ this.exportForm.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
- .subscribe(([value, isExportAttachmentsEnabled]) => {
+ .subscribe(([value]) => {
this.organizationId = value !== "myVault" ? value : undefined;
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");
this.exportForm.get("format").setValue("json");
- if (value === "myVault" && isExportAttachmentsEnabled) {
+ if (value === "myVault") {
this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" });
}
});
diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts
index 52d70e8652a..50577472120 100644
--- a/libs/vault/src/cipher-form/cipher-form.stories.ts
+++ b/libs/vault/src/cipher-form/cipher-form.stories.ts
@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { importProvidersFrom, signal } from "@angular/core";
+import { ActivatedRoute } from "@angular/router";
import { action } from "@storybook/addon-actions";
import {
applicationConfig,
@@ -225,6 +226,14 @@ export default {
getFeatureFlag: () => Promise.resolve(false),
},
},
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: {
+ queryParams: {},
+ },
+ },
+ },
],
}),
componentWrapperDecorator(
diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts
index 58a9b8f3965..af39ea96c16 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts
@@ -83,4 +83,24 @@ describe("AddEditCustomFieldDialogComponent", () => {
expect.objectContaining({ value: FieldType.Linked }),
);
});
+
+ it("does not filter out 'Hidden' field type when 'disallowHiddenField' is false", () => {
+ dialogData.disallowHiddenField = false;
+ fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent);
+ component = fixture.componentInstance;
+
+ expect(component.fieldTypeOptions).toContainEqual(
+ expect.objectContaining({ value: FieldType.Hidden }),
+ );
+ });
+
+ it("filers out 'Hidden' field type when 'disallowHiddenField' is true", () => {
+ dialogData.disallowHiddenField = true;
+ fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent);
+ component = fixture.componentInstance;
+
+ expect(component.fieldTypeOptions).not.toContainEqual(
+ expect.objectContaining({ value: FieldType.Hidden }),
+ );
+ });
});
diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts
index bdf5345672d..72bdf5dca1a 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts
@@ -25,6 +25,7 @@ export type AddEditCustomFieldDialogData = {
cipherType: CipherType;
/** When provided, dialog will display edit label variants */
editLabelConfig?: { index: number; label: string };
+ disallowHiddenField?: boolean;
};
@Component({
@@ -68,6 +69,9 @@ export class AddEditCustomFieldDialogComponent {
this.variant = data.editLabelConfig ? "edit" : "add";
this.fieldTypeOptions = this.fieldTypeOptions.filter((option) => {
+ if (this.data.disallowHiddenField && option.value === FieldType.Hidden) {
+ return false;
+ }
// Filter out the Linked field type for Secure Notes
if (this.data.cipherType === CipherType.SecureNote) {
return option.value !== FieldType.Linked;
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html
index 3bce3c5f385..1305bcdae05 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html
+++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html
@@ -89,7 +89,7 @@
bitIconButton="bwi-pencil-square"
class="tw-self-center tw-mt-2"
data-testid="edit-custom-field-button"
- *ngIf="!isPartialEdit"
+ *ngIf="canEdit(field.value.type)"
>
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts
index fb9664594ed..ced8763f895 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts
@@ -45,7 +45,9 @@ describe("CustomFieldsComponent", () => {
announce = jest.fn().mockResolvedValue(null);
patchCipher = jest.fn();
originalCipherView = new CipherView();
- config = {} as CipherFormConfig;
+ config = {
+ collections: [],
+ } as CipherFormConfig;
await TestBed.configureTestingModule({
imports: [CustomFieldsComponent],
@@ -463,5 +465,91 @@ describe("CustomFieldsComponent", () => {
// "reorder boolean label to position 4 of 4"
expect(announce).toHaveBeenCalledWith("reorderFieldDown boolean label 4 4", "assertive");
});
+
+ it("hides reorder buttons when in partial edit mode", () => {
+ originalCipherView.fields = mockFieldViews;
+ config.mode = "partial-edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ toggleItems = fixture.debugElement.queryAll(
+ By.css('button[data-testid="reorder-toggle-button"]'),
+ );
+
+ expect(toggleItems).toHaveLength(0);
+ });
+ });
+
+ it("shows all reorders button when in edit mode and viewPassword is true", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = true;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const toggleItems = fixture.debugElement.queryAll(
+ By.css('button[data-testid="reorder-toggle-button"]'),
+ );
+ expect(toggleItems).toHaveLength(4);
+ });
+
+ it("shows all reorder buttons except for hidden fields when in edit mode and viewPassword is false", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = false;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const toggleItems = fixture.debugElement.queryAll(
+ By.css('button[data-testid="reorder-toggle-button"]'),
+ );
+
+ expect(toggleItems).toHaveLength(3);
+ });
+
+ describe("edit button", () => {
+ it("hides the edit button when in partial-edit mode", () => {
+ originalCipherView.fields = mockFieldViews;
+ config.mode = "partial-edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const editButtons = fixture.debugElement.queryAll(
+ By.css('button[data-testid="edit-custom-field-button"]'),
+ );
+ expect(editButtons).toHaveLength(0);
+ });
+
+ it("shows all the edit buttons when in edit mode and viewPassword is true", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = true;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const editButtons = fixture.debugElement.queryAll(
+ By.css('button[data-testid="edit-custom-field-button"]'),
+ );
+ expect(editButtons).toHaveLength(4);
+ });
+
+ it("shows all the edit buttons except for hidden fields when in edit mode and viewPassword is false", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = false;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const editButtons = fixture.debugElement.queryAll(
+ By.css('button[data-testid="edit-custom-field-button"]'),
+ );
+ expect(editButtons).toHaveLength(3);
+ });
});
});
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts
index dd3fd8c24a8..49e9e109b74 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts
@@ -116,6 +116,8 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
/** Emits when a new custom field should be focused */
private focusOnNewInput$ = new Subject