mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 11:13:46 +00:00
[AC-1754] Provide upgrade flow for paid organizations (#6948)
* wip * Running prettier after npm ci * Defects AC-1929 AC-1955 AC-1956 * Updated logic to correctly set seat count depending on how you approach the upgrade flow * Setting sm seats when upgrading to the current count * Setting max storage if the organization's current plan has it set above the base * Refactored logic in changedProduct to be a bit more concise. Added logic for handling sm service accounts and storage increases * Decomposed the logic in changedProduct * Resolved defects introduced in the merge conflict --------- Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com> Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
This commit is contained in:
@@ -39,7 +39,6 @@ import {
|
||||
|
||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||
import { orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator } from "./validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator";
|
||||
import { orgWithoutAdditionalSeatLimitReachedWithoutUpgradePathValidator } from "./validators/org-without-additional-seat-limit-reached-without-upgrade-path.validator";
|
||||
|
||||
export enum MemberDialogTab {
|
||||
Role = 0,
|
||||
@@ -186,12 +185,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionFreePlan", organization.seats),
|
||||
),
|
||||
orgWithoutAdditionalSeatLimitReachedWithoutUpgradePathValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionFamiliesPlan", organization.seats),
|
||||
this.i18nService.t("subscriptionUpgrade", organization.seats),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toStrictEqual({ freePlanLimitReached: { message: errorMessage } });
|
||||
expect(result).toStrictEqual({ seatLimitReached: { message: errorMessage } });
|
||||
});
|
||||
|
||||
it("should return null when not on free plan", () => {
|
||||
|
||||
@@ -36,9 +36,14 @@ export function orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
),
|
||||
);
|
||||
|
||||
return organization.planProductType === ProductType.Free &&
|
||||
const productHasAdditionalSeatsOption =
|
||||
organization.planProductType !== ProductType.Free &&
|
||||
organization.planProductType !== ProductType.Families &&
|
||||
organization.planProductType !== ProductType.TeamsStarter;
|
||||
|
||||
return !productHasAdditionalSeatsOption &&
|
||||
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
|
||||
? { freePlanLimitReached: { message: errorMessage } }
|
||||
? { seatLimitReached: { message: errorMessage } }
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
/**
|
||||
* If the organization doesn't allow additional seat options, this checks if the seat limit has been reached when adding
|
||||
* new users
|
||||
* @param organization An object representing the organization
|
||||
* @param allOrganizationUserEmails An array of strings with existing user email addresses
|
||||
* @param errorMessage A localized string to display if validation fails
|
||||
* @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null`
|
||||
*/
|
||||
export function orgWithoutAdditionalSeatLimitReachedWithoutUpgradePathValidator(
|
||||
organization: Organization,
|
||||
allOrganizationUserEmails: string[],
|
||||
errorMessage: string,
|
||||
): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (control.value === "" || !control.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newEmailsToAdd = Array.from(
|
||||
new Set(
|
||||
control.value
|
||||
.split(",")
|
||||
.filter(
|
||||
(newEmailToAdd: string) =>
|
||||
newEmailToAdd &&
|
||||
newEmailToAdd.trim() !== "" &&
|
||||
!allOrganizationUserEmails.some(
|
||||
(existingEmail) => existingEmail === newEmailToAdd.trim(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return (organization.planProductType === ProductType.Families ||
|
||||
organization.planProductType === ProductType.TeamsStarter) &&
|
||||
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
|
||||
? { orgSeatLimitReachedWithoutUpgradePath: { message: errorMessage } }
|
||||
: null;
|
||||
};
|
||||
}
|
||||
@@ -376,17 +376,6 @@ export class PeopleComponent
|
||||
return `${product}InvLimitReached${this.getManageBillingText()}`;
|
||||
}
|
||||
|
||||
private getDialogTitle(productType: ProductType): string {
|
||||
switch (productType) {
|
||||
case ProductType.Free:
|
||||
return "upgrade";
|
||||
case ProductType.TeamsStarter:
|
||||
return "contactSupportShort";
|
||||
default:
|
||||
throw new Error(`Unsupported product type: ${productType}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getDialogContent(): string {
|
||||
return this.i18nService.t(
|
||||
this.getProductKey(this.organization.planProductType),
|
||||
@@ -399,7 +388,13 @@ export class PeopleComponent
|
||||
return this.i18nService.t("ok");
|
||||
}
|
||||
|
||||
return this.i18nService.t(this.getDialogTitle(this.organization.planProductType));
|
||||
const productType = this.organization.planProductType;
|
||||
|
||||
if (productType !== ProductType.Free && productType !== ProductType.TeamsStarter) {
|
||||
throw new Error(`Unsupported product type: ${productType}`);
|
||||
}
|
||||
|
||||
return this.i18nService.t("upgrade");
|
||||
}
|
||||
|
||||
private async handleDialogClose(result: boolean | undefined): Promise<void> {
|
||||
@@ -407,19 +402,16 @@ export class PeopleComponent
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.organization.planProductType) {
|
||||
case ProductType.Free:
|
||||
await this.router.navigate(
|
||||
["/organizations", this.organization.id, "billing", "subscription"],
|
||||
{ queryParams: { upgrade: true } },
|
||||
);
|
||||
break;
|
||||
case ProductType.TeamsStarter:
|
||||
window.open("https://bitwarden.com/contact/", "_blank");
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported product type: ${this.organization.planProductType}`);
|
||||
const productType = this.organization.planProductType;
|
||||
|
||||
if (productType !== ProductType.Free && productType !== ProductType.TeamsStarter) {
|
||||
throw new Error(`Unsupported product type: ${this.organization.planProductType}`);
|
||||
}
|
||||
|
||||
await this.router.navigate(
|
||||
["/organizations", this.organization.id, "billing", "subscription"],
|
||||
{ queryParams: { upgrade: true } },
|
||||
);
|
||||
}
|
||||
|
||||
private async showSeatLimitReachedDialog(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user