[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:
parent
690f4a0ae9
commit
3d30823d2a
|
@ -39,7 +39,6 @@ import {
|
||||||
|
|
||||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||||
import { orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator } from "./validators/org-without-additional-seat-limit-reached-with-upgrade-path.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 {
|
export enum MemberDialogTab {
|
||||||
Role = 0,
|
Role = 0,
|
||||||
|
@ -186,12 +185,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||||
this.organization,
|
this.organization,
|
||||||
this.params.allOrganizationUserEmails,
|
this.params.allOrganizationUserEmails,
|
||||||
this.i18nService.t("subscriptionFreePlan", organization.seats),
|
this.i18nService.t("subscriptionUpgrade", organization.seats),
|
||||||
),
|
|
||||||
orgWithoutAdditionalSeatLimitReachedWithoutUpgradePathValidator(
|
|
||||||
this.organization,
|
|
||||||
this.params.allOrganizationUserEmails,
|
|
||||||
this.i18nService.t("subscriptionFamiliesPlan", organization.seats),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||||
|
|
||||||
const result = validatorFn(control);
|
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", () => {
|
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
|
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
|
||||||
? { freePlanLimitReached: { message: errorMessage } }
|
? { seatLimitReached: { message: errorMessage } }
|
||||||
: null;
|
: 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()}`;
|
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 {
|
private getDialogContent(): string {
|
||||||
return this.i18nService.t(
|
return this.i18nService.t(
|
||||||
this.getProductKey(this.organization.planProductType),
|
this.getProductKey(this.organization.planProductType),
|
||||||
|
@ -399,7 +388,13 @@ export class PeopleComponent
|
||||||
return this.i18nService.t("ok");
|
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> {
|
private async handleDialogClose(result: boolean | undefined): Promise<void> {
|
||||||
|
@ -407,19 +402,16 @@ export class PeopleComponent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.organization.planProductType) {
|
const productType = this.organization.planProductType;
|
||||||
case ProductType.Free:
|
|
||||||
|
if (productType !== ProductType.Free && productType !== ProductType.TeamsStarter) {
|
||||||
|
throw new Error(`Unsupported product type: ${this.organization.planProductType}`);
|
||||||
|
}
|
||||||
|
|
||||||
await this.router.navigate(
|
await this.router.navigate(
|
||||||
["/organizations", this.organization.id, "billing", "subscription"],
|
["/organizations", this.organization.id, "billing", "subscription"],
|
||||||
{ queryParams: { upgrade: true } },
|
{ 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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showSeatLimitReachedDialog(): Promise<void> {
|
private async showSeatLimitReachedDialog(): Promise<void> {
|
||||||
|
|
|
@ -8,10 +8,8 @@
|
||||||
<app-organization-plans
|
<app-organization-plans
|
||||||
[showFree]="false"
|
[showFree]="false"
|
||||||
[showCancel]="true"
|
[showCancel]="true"
|
||||||
[plan]="defaultUpgradePlan"
|
|
||||||
[product]="defaultUpgradeProduct"
|
|
||||||
[organizationId]="organizationId"
|
[organizationId]="organizationId"
|
||||||
[currentProductType]="currentProductType"
|
[currentPlan]="currentPlan"
|
||||||
(onCanceled)="cancel()"
|
(onCanceled)="cancel()"
|
||||||
>
|
>
|
||||||
</app-organization-plans>
|
</app-organization-plans>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -10,13 +9,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||||
})
|
})
|
||||||
export class ChangePlanComponent {
|
export class ChangePlanComponent {
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Input() currentProductType: ProductType;
|
@Input() currentPlan: PlanResponse;
|
||||||
@Output() onChanged = new EventEmitter();
|
@Output() onChanged = new EventEmitter();
|
||||||
@Output() onCanceled = new EventEmitter();
|
@Output() onCanceled = new EventEmitter();
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
|
|
||||||
defaultUpgradeProduct: ProductType = ProductType.Families;
|
|
||||||
|
|
||||||
constructor(private logService: LogService) {}
|
constructor(private logService: LogService) {}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { ProviderResponse } from "@bitwarden/common/admin-console/models/respons
|
||||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||||
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
||||||
|
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
@ -69,7 +70,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
@Input() showFree = true;
|
@Input() showFree = true;
|
||||||
@Input() showCancel = false;
|
@Input() showCancel = false;
|
||||||
@Input() acceptingSponsorship = false;
|
@Input() acceptingSponsorship = false;
|
||||||
@Input() currentProductType: ProductType;
|
@Input() currentPlan: PlanResponse;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get product(): ProductType {
|
get product(): ProductType {
|
||||||
|
@ -126,6 +127,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
passwordManagerPlans: PlanResponse[];
|
passwordManagerPlans: PlanResponse[];
|
||||||
secretsManagerPlans: PlanResponse[];
|
secretsManagerPlans: PlanResponse[];
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
|
sub: OrganizationSubscriptionResponse;
|
||||||
billing: BillingResponse;
|
billing: BillingResponse;
|
||||||
provider: ProviderResponse;
|
provider: ProviderResponse;
|
||||||
|
|
||||||
|
@ -149,6 +151,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
if (this.organizationId) {
|
||||||
|
this.organization = this.organizationService.get(this.organizationId);
|
||||||
|
this.billing = await this.organizationApiService.getBilling(this.organizationId);
|
||||||
|
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.selfHosted) {
|
if (!this.selfHosted) {
|
||||||
const plans = await this.apiService.getPlans();
|
const plans = await this.apiService.getPlans();
|
||||||
this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager);
|
this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager);
|
||||||
|
@ -179,11 +187,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.organizationId) {
|
|
||||||
this.organization = this.organizationService.get(this.organizationId);
|
|
||||||
this.billing = await this.organizationApiService.getBilling(this.organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,10 +257,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
(plan.isAnnual ||
|
(plan.isAnnual ||
|
||||||
plan.product === ProductType.Free ||
|
plan.product === ProductType.Free ||
|
||||||
plan.product === ProductType.TeamsStarter) &&
|
plan.product === ProductType.TeamsStarter) &&
|
||||||
(this.currentProductType !== ProductType.TeamsStarter ||
|
(!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) &&
|
||||||
plan.product === ProductType.Teams ||
|
(!this.hasProvider || plan.product !== ProductType.TeamsStarter) &&
|
||||||
plan.product === ProductType.Enterprise) &&
|
|
||||||
(!this.providerId || plan.product !== ProductType.TeamsStarter) &&
|
|
||||||
((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) ||
|
((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) ||
|
||||||
(this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))),
|
(this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))),
|
||||||
);
|
);
|
||||||
|
@ -269,16 +270,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
get selectablePlans() {
|
get selectablePlans() {
|
||||||
const selectedProductType = this.formGroup.controls.product.value;
|
const selectedProductType = this.formGroup.controls.product.value;
|
||||||
const result = this.passwordManagerPlans?.filter((plan) => {
|
const result = this.passwordManagerPlans?.filter(
|
||||||
const productMatch = plan.product === selectedProductType;
|
(plan) =>
|
||||||
|
plan.product === selectedProductType &&
|
||||||
return (
|
((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) ||
|
||||||
(!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan) && productMatch) ||
|
(this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))),
|
||||||
(this.isProviderQualifiedFor2020Plan() &&
|
|
||||||
Allowed2020PlanTypes.includes(plan.type) &&
|
|
||||||
productMatch)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder);
|
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder);
|
||||||
return result;
|
return result;
|
||||||
|
@ -415,27 +412,64 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
changedProduct() {
|
changedProduct() {
|
||||||
this.formGroup.controls.plan.setValue(this.selectablePlans[0].type);
|
const selectedPlan = this.selectablePlans[0];
|
||||||
if (!this.selectedPlan.PasswordManager.hasPremiumAccessOption) {
|
|
||||||
this.formGroup.controls.premiumAccessAddon.setValue(false);
|
this.setPlanType(selectedPlan.type);
|
||||||
}
|
this.handlePremiumAddonAccess(selectedPlan.PasswordManager.hasPremiumAccessOption);
|
||||||
if (!this.selectedPlan.PasswordManager.hasAdditionalStorageOption) {
|
this.handleAdditionalStorage(selectedPlan.PasswordManager.hasAdditionalStorageOption);
|
||||||
this.formGroup.controls.additionalStorage.setValue(0);
|
this.handleAdditionalSeats(selectedPlan.PasswordManager.hasAdditionalSeatsOption);
|
||||||
}
|
this.handleSecretsManagerForm();
|
||||||
if (!this.selectedPlan.PasswordManager.hasAdditionalSeatsOption) {
|
|
||||||
this.formGroup.controls.additionalSeats.setValue(0);
|
|
||||||
} else if (
|
|
||||||
!this.formGroup.controls.additionalSeats.value &&
|
|
||||||
!this.selectedPlan.PasswordManager.baseSeats &&
|
|
||||||
this.selectedPlan.PasswordManager.hasAdditionalSeatsOption
|
|
||||||
) {
|
|
||||||
this.formGroup.controls.additionalSeats.setValue(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPlanType(planType: PlanType) {
|
||||||
|
this.formGroup.controls.plan.setValue(planType);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePremiumAddonAccess(hasPremiumAccessOption: boolean) {
|
||||||
|
this.formGroup.controls.premiumAccessAddon.setValue(!hasPremiumAccessOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAdditionalStorage(selectedPlanHasAdditionalStorageOption: boolean) {
|
||||||
|
if (!selectedPlanHasAdditionalStorageOption || !this.currentPlan) {
|
||||||
|
this.formGroup.controls.additionalStorage.setValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.organization?.maxStorageGb) {
|
||||||
|
this.formGroup.controls.additionalStorage.setValue(
|
||||||
|
this.organization.maxStorageGb - this.currentPlan.PasswordManager.baseStorageGb,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAdditionalSeats(selectedPlanHasAdditionalSeatsOption: boolean) {
|
||||||
|
if (!selectedPlanHasAdditionalSeatsOption) {
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.currentPlan?.PasswordManager?.hasAdditionalSeatsOption) {
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(this.currentPlan.PasswordManager.baseSeats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(this.organization.seats);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSecretsManagerForm() {
|
||||||
if (this.planOffersSecretsManager) {
|
if (this.planOffersSecretsManager) {
|
||||||
this.secretsManagerForm.enable();
|
this.secretsManagerForm.enable();
|
||||||
} else {
|
}
|
||||||
this.secretsManagerForm.disable();
|
|
||||||
|
if (this.organization.useSecretsManager) {
|
||||||
|
this.secretsManagerForm.controls.enabled.setValue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.secretsManagerForm.controls.enabled.value) {
|
||||||
|
this.secretsManagerForm.controls.userSeats.setValue(this.sub?.smSeats || 1);
|
||||||
|
this.secretsManagerForm.controls.additionalServiceAccounts.setValue(
|
||||||
|
this.sub?.smServiceAccounts - this.currentPlan.SecretsManager?.baseServiceAccount || 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.secretsManagerForm.updateValueAndValidity();
|
this.secretsManagerForm.updateValueAndValidity();
|
||||||
|
@ -700,11 +734,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If they already have SM enabled, bump them up to Teams and enable SM to maintain this access
|
if (this.currentPlan && this.currentPlan.product !== ProductType.Enterprise) {
|
||||||
const organization = this.organizationService.get(this.organizationId);
|
const upgradedPlan = this.passwordManagerPlans.find((plan) =>
|
||||||
if (organization.useSecretsManager) {
|
this.currentPlan.product === ProductType.Free
|
||||||
this.formGroup.controls.product.setValue(ProductType.Teams);
|
? plan.type === PlanType.FamiliesAnnually
|
||||||
this.secretsManagerForm.controls.enabled.setValue(true);
|
: plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.plan = upgradedPlan.type;
|
||||||
|
this.product = upgradedPlan.product;
|
||||||
this.changedProduct();
|
this.changedProduct();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@
|
||||||
</button>
|
</button>
|
||||||
<app-change-plan
|
<app-change-plan
|
||||||
[organizationId]="organizationId"
|
[organizationId]="organizationId"
|
||||||
[currentProductType]="sub.plan.product"
|
[currentPlan]="sub.plan"
|
||||||
(onChanged)="closeChangePlan()"
|
(onChanged)="closeChangePlan()"
|
||||||
(onCanceled)="closeChangePlan()"
|
(onCanceled)="closeChangePlan()"
|
||||||
*ngIf="showChangePlan"
|
*ngIf="showChangePlan"
|
||||||
|
|
|
@ -249,12 +249,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
|
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
|
||||||
} else if (
|
} else if (
|
||||||
this.sub.planType === PlanType.FamiliesAnnually ||
|
this.sub.planType === PlanType.FamiliesAnnually ||
|
||||||
this.sub.planType === PlanType.FamiliesAnnually2019
|
this.sub.planType === PlanType.FamiliesAnnually2019 ||
|
||||||
|
this.sub.planType === PlanType.TeamsStarter
|
||||||
) {
|
) {
|
||||||
if (this.isSponsoredSubscription) {
|
if (this.isSponsoredSubscription) {
|
||||||
return this.i18nService.t("subscriptionSponsoredFamiliesPlan", this.sub.seats.toString());
|
return this.i18nService.t("subscriptionSponsoredFamiliesPlan", this.sub.seats.toString());
|
||||||
} else {
|
} else {
|
||||||
return this.i18nService.t("subscriptionFamiliesPlan", this.sub.seats.toString());
|
return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString());
|
||||||
}
|
}
|
||||||
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
|
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
|
||||||
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
|
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
|
||||||
|
@ -413,7 +414,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
};
|
};
|
||||||
|
|
||||||
get showChangePlanButton() {
|
get showChangePlanButton() {
|
||||||
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
return this.sub.plan.product !== ProductType.Enterprise && !this.showChangePlan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3548,12 +3548,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subscriptionFamiliesPlan": {
|
"subscriptionUpgrade": {
|
||||||
"message": "You cannot invite more than $COUNT$ members without upgrading your plan. Please contact Customer Support to upgrade.",
|
"message": "You cannot invite more than $COUNT$ members without upgrading your plan.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "6"
|
"example": "2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6668,7 +6668,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"teamsStarterPlanInvLimitReachedManageBilling": {
|
"teamsStarterPlanInvLimitReachedManageBilling": {
|
||||||
"message": "Teams Starter plans may have up to $SEATCOUNT$ members. Contact Customer Support to upgrade your plan and invite more members.",
|
"message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"seatcount": {
|
"seatcount": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
|
|
Loading…
Reference in New Issue