-
{{ send.file.fileName }} ({{ send.file.sizeName }})
+
+ {{ send.file.fileName }} ({{ send.file.sizeName }})
+
diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts
index 1f58b03dbda..c50964e31e3 100644
--- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts
+++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts
@@ -448,10 +448,10 @@ export class DesktopAutofillService implements OnDestroy {
function normalizePosition(position: { x: number; y: number }): { x: number; y: number } {
// Add 100 pixels to the x-coordinate to offset the native OS dialog positioning.
- const xPostionOffset = 100;
+ const xPositionOffset = 100;
return {
- x: Math.round(position.x + xPostionOffset),
+ x: Math.round(position.x + xPositionOffset),
y: Math.round(position.y),
};
}
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 33582c857aa..92e350fab90 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -708,6 +708,15 @@
"addAttachment": {
"message": "Add attachment"
},
+ "fixEncryption": {
+ "message": "Fix encryption"
+ },
+ "fixEncryptionTooltip": {
+ "message": "This file is using an outdated encryption method."
+ },
+ "attachmentUpdated": {
+ "message": "Attachment updated"
+ },
"maxFileSizeSansPunctuation": {
"message": "Maximum file size is 500 MB"
},
diff --git a/apps/desktop/src/scss/migration.scss b/apps/desktop/src/scss/migration.scss
new file mode 100644
index 00000000000..e3078158283
--- /dev/null
+++ b/apps/desktop/src/scss/migration.scss
@@ -0,0 +1,15 @@
+/**
+ * Desktop UI Migration
+ *
+ * These are temporary styles during the desktop ui migration.
+ **/
+
+/**
+ * This removes any padding applied by the bit-layout to content.
+ * This should be revisited once the table is migrated, and again once drawers are migrated.
+ **/
+bit-layout {
+ #main-content {
+ padding: 0 0 0 0;
+ }
+}
diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss
index c579e6acdc0..b4082afd38c 100644
--- a/apps/desktop/src/scss/styles.scss
+++ b/apps/desktop/src/scss/styles.scss
@@ -15,5 +15,6 @@
@import "left-nav.scss";
@import "loading.scss";
@import "plugins.scss";
+@import "migration.scss";
@import "../../../../libs/angular/src/scss/icons.scss";
@import "../../../../libs/components/src/multi-select/scss/bw.theme";
diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.spec.ts b/apps/web/src/app/admin-console/common/people-table-data-source.spec.ts
new file mode 100644
index 00000000000..e9cf87a114d
--- /dev/null
+++ b/apps/web/src/app/admin-console/common/people-table-data-source.spec.ts
@@ -0,0 +1,130 @@
+import { TestBed } from "@angular/core/testing";
+import { ReplaySubject } from "rxjs";
+
+import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import {
+ Environment,
+ EnvironmentService,
+} from "@bitwarden/common/platform/abstractions/environment.service";
+
+import { PeopleTableDataSource } from "./people-table-data-source";
+
+interface MockUser {
+ id: string;
+ name: string;
+ email: string;
+ status: OrganizationUserStatusType;
+ checked?: boolean;
+}
+
+class TestPeopleTableDataSource extends PeopleTableDataSource
{
+ protected statusType = OrganizationUserStatusType;
+}
+
+describe("PeopleTableDataSource", () => {
+ let dataSource: TestPeopleTableDataSource;
+
+ const createMockUser = (id: string, checked: boolean = false): MockUser => ({
+ id,
+ name: `User ${id}`,
+ email: `user${id}@example.com`,
+ status: OrganizationUserStatusType.Confirmed,
+ checked,
+ });
+
+ const createMockUsers = (count: number, checked: boolean = false): MockUser[] => {
+ return Array.from({ length: count }, (_, i) => createMockUser(`${i + 1}`, checked));
+ };
+
+ beforeEach(() => {
+ const featureFlagSubject = new ReplaySubject(1);
+ featureFlagSubject.next(false);
+
+ const environmentSubject = new ReplaySubject(1);
+ environmentSubject.next({
+ isCloud: () => false,
+ } as Environment);
+
+ const mockConfigService = {
+ getFeatureFlag$: jest.fn(() => featureFlagSubject.asObservable()),
+ } as any;
+
+ const mockEnvironmentService = {
+ environment$: environmentSubject.asObservable(),
+ } as any;
+
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: ConfigService, useValue: mockConfigService },
+ { provide: EnvironmentService, useValue: mockEnvironmentService },
+ ],
+ });
+
+ dataSource = TestBed.runInInjectionContext(
+ () => new TestPeopleTableDataSource(mockConfigService, mockEnvironmentService),
+ );
+ });
+
+ describe("limitAndUncheckExcess", () => {
+ it("should return all users when under limit", () => {
+ const users = createMockUsers(10, true);
+ dataSource.data = users;
+
+ const result = dataSource.limitAndUncheckExcess(users, 500);
+
+ expect(result).toHaveLength(10);
+ expect(result).toEqual(users);
+ expect(users.every((u) => u.checked)).toBe(true);
+ });
+
+ it("should limit users and uncheck excess", () => {
+ const users = createMockUsers(600, true);
+ dataSource.data = users;
+
+ const result = dataSource.limitAndUncheckExcess(users, 500);
+
+ expect(result).toHaveLength(500);
+ expect(result).toEqual(users.slice(0, 500));
+ expect(users.slice(0, 500).every((u) => u.checked)).toBe(true);
+ expect(users.slice(500).every((u) => u.checked)).toBe(false);
+ });
+
+ it("should only affect users in the provided array", () => {
+ const allUsers = createMockUsers(1000, true);
+ dataSource.data = allUsers;
+
+ // Pass only a subset (simulates filtering by status)
+ const subset = allUsers.slice(0, 600);
+
+ const result = dataSource.limitAndUncheckExcess(subset, 500);
+
+ expect(result).toHaveLength(500);
+ expect(subset.slice(0, 500).every((u) => u.checked)).toBe(true);
+ expect(subset.slice(500).every((u) => u.checked)).toBe(false);
+ // Users outside subset remain checked
+ expect(allUsers.slice(600).every((u) => u.checked)).toBe(true);
+ });
+ });
+
+ describe("status counts", () => {
+ it("should correctly count users by status", () => {
+ const users: MockUser[] = [
+ { ...createMockUser("1"), status: OrganizationUserStatusType.Invited },
+ { ...createMockUser("2"), status: OrganizationUserStatusType.Invited },
+ { ...createMockUser("3"), status: OrganizationUserStatusType.Accepted },
+ { ...createMockUser("4"), status: OrganizationUserStatusType.Confirmed },
+ { ...createMockUser("5"), status: OrganizationUserStatusType.Confirmed },
+ { ...createMockUser("6"), status: OrganizationUserStatusType.Confirmed },
+ { ...createMockUser("7"), status: OrganizationUserStatusType.Revoked },
+ ];
+ dataSource.data = users;
+
+ expect(dataSource.invitedUserCount).toBe(2);
+ expect(dataSource.acceptedUserCount).toBe(1);
+ expect(dataSource.confirmedUserCount).toBe(3);
+ expect(dataSource.revokedUserCount).toBe(1);
+ expect(dataSource.activeUserCount).toBe(6); // All except revoked
+ });
+ });
+});
diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts
index 4696f8a6738..0228edb1e8c 100644
--- a/apps/web/src/app/admin-console/common/people-table-data-source.ts
+++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts
@@ -1,14 +1,30 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import { computed, Signal } from "@angular/core";
+import { toSignal } from "@angular/core/rxjs-interop";
+import { map } from "rxjs";
+
import {
OrganizationUserStatusType,
ProviderUserStatusType,
} from "@bitwarden/common/admin-console/enums";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { TableDataSource } from "@bitwarden/components";
import { StatusType, UserViewTypes } from "./base-members.component";
-const MaxCheckedCount = 500;
+/**
+ * Default maximum for most bulk operations (confirm, remove, delete, etc.)
+ */
+export const MaxCheckedCount = 500;
+
+/**
+ * Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
+ * feature flag is enabled on cloud environments.
+ */
+export const CloudBulkReinviteLimit = 4000;
/**
* Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked).
@@ -56,6 +72,20 @@ export abstract class PeopleTableDataSource extends Tab
confirmedUserCount: number;
revokedUserCount: number;
+ /** True when increased bulk limit feature is enabled (feature flag + cloud environment) */
+ readonly isIncreasedBulkLimitEnabled: Signal;
+
+ constructor(configService: ConfigService, environmentService: EnvironmentService) {
+ super();
+
+ const featureFlagEnabled = toSignal(
+ configService.getFeatureFlag$(FeatureFlag.IncreaseBulkReinviteLimitForCloud),
+ );
+ const isCloud = toSignal(environmentService.environment$.pipe(map((env) => env.isCloud())));
+
+ this.isIncreasedBulkLimitEnabled = computed(() => featureFlagEnabled() && isCloud());
+ }
+
override set data(data: T[]) {
super.data = data;
@@ -89,6 +119,14 @@ export abstract class PeopleTableDataSource extends Tab
return this.data.filter((u) => (u as any).checked);
}
+ /**
+ * Gets checked users in the order they appear in the filtered/sorted table view.
+ * Use this when enforcing limits to ensure visual consistency (top N visible rows stay checked).
+ */
+ getCheckedUsersInVisibleOrder() {
+ return this.filteredData.filter((u) => (u as any).checked);
+ }
+
/**
* Check all filtered users (i.e. those rows that are currently visible)
* @param select check the filtered users (true) or uncheck the filtered users (false)
@@ -101,8 +139,13 @@ export abstract class PeopleTableDataSource extends Tab
const filteredUsers = this.filteredData;
- const selectCount =
- filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
+ // When the increased bulk limit feature is enabled, allow checking all users.
+ // Individual bulk operations will enforce their specific limits.
+ // When disabled, enforce the legacy limit at check time.
+ const selectCount = this.isIncreasedBulkLimitEnabled()
+ ? filteredUsers.length
+ : Math.min(filteredUsers.length, MaxCheckedCount);
+
for (let i = 0; i < selectCount; i++) {
this.checkUser(filteredUsers[i], select);
}
@@ -132,4 +175,41 @@ export abstract class PeopleTableDataSource extends Tab
this.data = updatedData;
}
}
+
+ /**
+ * Limits an array of users and unchecks those beyond the limit.
+ * Returns the limited array.
+ *
+ * @param users The array of users to limit
+ * @param limit The maximum number of users to keep
+ * @returns The users array limited to the specified count
+ */
+ limitAndUncheckExcess(users: T[], limit: number): T[] {
+ if (users.length <= limit) {
+ return users;
+ }
+
+ // Uncheck users beyond the limit
+ users.slice(limit).forEach((user) => this.checkUser(user, false));
+
+ return users.slice(0, limit);
+ }
+
+ /**
+ * Gets checked users with optional limiting based on the IncreaseBulkReinviteLimitForCloud feature flag.
+ *
+ * When the feature flag is enabled: Returns checked users in visible order, limited to the specified count.
+ * When the feature flag is disabled: Returns all checked users without applying any limit.
+ *
+ * @param limit The maximum number of users to return (only applied when feature flag is enabled)
+ * @returns The checked users array
+ */
+ getCheckedUsersWithLimit(limit: number): T[] {
+ if (this.isIncreasedBulkLimitEnabled()) {
+ const allUsers = this.getCheckedUsersInVisibleOrder();
+ return this.limitAndUncheckExcess(allUsers, limit);
+ } else {
+ return this.getCheckedUsers();
+ }
+ }
}
diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts
index 59c4c4898ea..ac25278a636 100644
--- a/apps/web/src/app/admin-console/organizations/members/members.component.ts
+++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts
@@ -33,6 +33,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -44,7 +46,11 @@ import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/membe
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { BaseMembersComponent } from "../../common/base-members.component";
-import { PeopleTableDataSource } from "../../common/people-table-data-source";
+import {
+ CloudBulkReinviteLimit,
+ MaxCheckedCount,
+ PeopleTableDataSource,
+} from "../../common/people-table-data-source";
import { OrganizationUserView } from "../core/views/organization-user.view";
import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component";
@@ -70,7 +76,7 @@ export class MembersComponent extends BaseMembersComponent
userType = OrganizationUserType;
userStatusType = OrganizationUserStatusType;
memberTab = MemberDialogTab;
- protected dataSource = new MembersTableDataSource();
+ protected dataSource: MembersTableDataSource;
readonly organization: Signal;
status: OrganizationUserStatusType | undefined;
@@ -113,6 +119,8 @@ export class MembersComponent extends BaseMembersComponent
private policyService: PolicyService,
private policyApiService: PolicyApiServiceAbstraction,
private organizationMetadataService: OrganizationMetadataServiceAbstraction,
+ private configService: ConfigService,
+ private environmentService: EnvironmentService,
) {
super(
apiService,
@@ -126,6 +134,8 @@ export class MembersComponent extends BaseMembersComponent
toastService,
);
+ this.dataSource = new MembersTableDataSource(this.configService, this.environmentService);
+
const organization$ = this.route.params.pipe(
concatMap((params) =>
this.userId$.pipe(
@@ -356,10 +366,9 @@ export class MembersComponent extends BaseMembersComponent
return;
}
- await this.memberDialogManager.openBulkRemoveDialog(
- organization,
- this.dataSource.getCheckedUsers(),
- );
+ const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
+
+ await this.memberDialogManager.openBulkRemoveDialog(organization, users);
this.organizationMetadataService.refreshMetadataCache();
await this.load(organization);
}
@@ -369,10 +378,9 @@ export class MembersComponent extends BaseMembersComponent
return;
}
- await this.memberDialogManager.openBulkDeleteDialog(
- organization,
- this.dataSource.getCheckedUsers(),
- );
+ const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
+
+ await this.memberDialogManager.openBulkDeleteDialog(organization, users);
await this.load(organization);
}
@@ -389,11 +397,9 @@ export class MembersComponent extends BaseMembersComponent
return;
}
- await this.memberDialogManager.openBulkRestoreRevokeDialog(
- organization,
- this.dataSource.getCheckedUsers(),
- isRevoking,
- );
+ const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
+
+ await this.memberDialogManager.openBulkRestoreRevokeDialog(organization, users, isRevoking);
await this.load(organization);
}
@@ -402,8 +408,28 @@ export class MembersComponent extends BaseMembersComponent
return;
}
- const users = this.dataSource.getCheckedUsers();
- const filteredUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
+ let users: OrganizationUserView[];
+ if (this.dataSource.isIncreasedBulkLimitEnabled()) {
+ users = this.dataSource.getCheckedUsersInVisibleOrder();
+ } else {
+ users = this.dataSource.getCheckedUsers();
+ }
+
+ const allInvitedUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
+
+ // Capture the original count BEFORE enforcing the limit
+ const originalInvitedCount = allInvitedUsers.length;
+
+ // When feature flag is enabled, limit invited users and uncheck the excess
+ let filteredUsers: OrganizationUserView[];
+ if (this.dataSource.isIncreasedBulkLimitEnabled()) {
+ filteredUsers = this.dataSource.limitAndUncheckExcess(
+ allInvitedUsers,
+ CloudBulkReinviteLimit,
+ );
+ } else {
+ filteredUsers = allInvitedUsers;
+ }
if (filteredUsers.length <= 0) {
this.toastService.showToast({
@@ -424,13 +450,37 @@ export class MembersComponent extends BaseMembersComponent
throw new Error();
}
- // Bulk Status component open
- await this.memberDialogManager.openBulkStatusDialog(
- users,
- filteredUsers,
- Promise.resolve(result.successful),
- this.i18nService.t("bulkReinviteMessage"),
- );
+ // When feature flag is enabled, show toast instead of dialog
+ if (this.dataSource.isIncreasedBulkLimitEnabled()) {
+ const selectedCount = originalInvitedCount;
+ const invitedCount = filteredUsers.length;
+
+ if (selectedCount > CloudBulkReinviteLimit) {
+ const excludedCount = selectedCount - CloudBulkReinviteLimit;
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t(
+ "bulkReinviteLimitedSuccessToast",
+ CloudBulkReinviteLimit.toLocaleString(),
+ selectedCount.toLocaleString(),
+ excludedCount.toLocaleString(),
+ ),
+ });
+ } else {
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()),
+ });
+ }
+ } else {
+ // Feature flag disabled - show legacy dialog
+ await this.memberDialogManager.openBulkStatusDialog(
+ users,
+ filteredUsers,
+ Promise.resolve(result.successful),
+ this.i18nService.t("bulkReinviteMessage"),
+ );
+ }
} catch (e) {
this.validationService.showError(e);
}
@@ -442,15 +492,14 @@ export class MembersComponent extends BaseMembersComponent
return;
}
- await this.memberDialogManager.openBulkConfirmDialog(
- organization,
- this.dataSource.getCheckedUsers(),
- );
+ const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
+
+ await this.memberDialogManager.openBulkConfirmDialog(organization, users);
await this.load(organization);
}
async bulkEnableSM(organization: Organization) {
- const users = this.dataSource.getCheckedUsers();
+ const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users);
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.ts
index d78451e4f3a..aac7fd3156f 100644
--- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.ts
+++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.ts
@@ -157,7 +157,7 @@ export class CloudHostedPremiumVNextComponent {
return {
tier,
price:
- tier?.passwordManager.type === "standalone"
+ tier?.passwordManager.type === "standalone" && tier.passwordManager.annualPrice
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
: 0,
features: tier?.passwordManager.features.map((f) => f.value) || [],
@@ -172,7 +172,7 @@ export class CloudHostedPremiumVNextComponent {
return {
tier,
price:
- tier?.passwordManager.type === "packaged"
+ tier?.passwordManager.type === "packaged" && tier.passwordManager.annualPrice
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
: 0,
features: tier?.passwordManager.features.map((f) => f.value) || [],
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts
index a4089d7a47a..2ac44ff72db 100644
--- a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts
+++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts
@@ -1,15 +1,15 @@
import { CdkTrapFocus } from "@angular/cdk/a11y";
import { CommonModule } from "@angular/common";
-import { Component, DestroyRef, OnInit, computed, input, output, signal } from "@angular/core";
+import { Component, computed, DestroyRef, input, OnInit, output, signal } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { catchError, of } from "rxjs";
+import { SubscriptionPricingCardDetails } from "@bitwarden/angular/billing/types/subscription-pricing-card-details";
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
import {
PersonalSubscriptionPricingTier,
PersonalSubscriptionPricingTierId,
PersonalSubscriptionPricingTierIds,
- SubscriptionCadence,
SubscriptionCadenceIds,
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -32,14 +32,6 @@ export type UpgradeAccountResult = {
plan: PersonalSubscriptionPricingTierId | null;
};
-type CardDetails = {
- title: string;
- tagline: string;
- price: { amount: number; cadence: SubscriptionCadence };
- button: { text: string; type: ButtonType };
- features: string[];
-};
-
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -60,8 +52,8 @@ export class UpgradeAccountComponent implements OnInit {
planSelected = output();
closeClicked = output();
protected readonly loading = signal(true);
- protected premiumCardDetails!: CardDetails;
- protected familiesCardDetails!: CardDetails;
+ protected premiumCardDetails!: SubscriptionPricingCardDetails;
+ protected familiesCardDetails!: SubscriptionPricingCardDetails;
protected familiesPlanType = PersonalSubscriptionPricingTierIds.Families;
protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium;
@@ -122,14 +114,16 @@ export class UpgradeAccountComponent implements OnInit {
private createCardDetails(
tier: PersonalSubscriptionPricingTier,
buttonType: ButtonType,
- ): CardDetails {
+ ): SubscriptionPricingCardDetails {
return {
title: tier.name,
tagline: tier.description,
- price: {
- amount: tier.passwordManager.annualPrice / 12,
- cadence: SubscriptionCadenceIds.Monthly,
- },
+ price: tier.passwordManager.annualPrice
+ ? {
+ amount: tier.passwordManager.annualPrice / 12,
+ cadence: SubscriptionCadenceIds.Monthly,
+ }
+ : undefined,
button: {
text: this.i18nService.t(
this.isFamiliesPlan(tier.id) ? "startFreeFamiliesTrial" : "upgradeToPremium",
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts
index 94f1c816168..ae18ab4c629 100644
--- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts
+++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts
@@ -200,7 +200,8 @@ export class UpgradePaymentService {
}
private getPasswordManagerSeats(planDetails: PlanDetails): number {
- return "users" in planDetails.details.passwordManager
+ return "users" in planDetails.details.passwordManager &&
+ planDetails.details.passwordManager.users
? planDetails.details.passwordManager.users
: 0;
}
diff --git a/apps/web/src/app/tools/send/send-access/send-access-file.component.html b/apps/web/src/app/tools/send/send-access/send-access-file.component.html
index 82880407809..8cbe6a975ef 100644
--- a/apps/web/src/app/tools/send/send-access/send-access-file.component.html
+++ b/apps/web/src/app/tools/send/send-access/send-access-file.component.html
@@ -1,4 +1,4 @@
-{{ send.file.fileName }}
+{{ send.file.fileName }}