mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[AC-1708] Teams Starter Plan (#6740)
* Added support for the teams starter plan * Plans now respect display sort order. Updated teams starter to be in its own product * Remove upgrade button and show new copy instead -- wip copy * Added upgrade dialog for teams starter plan when adding an 11th user * Updated the add user validator to check if plan is teams starter. Updated to not count duplicated emails in the overall count * Renamed validator to be more descriptive and added additional unit tests * Added validator for org types that require customer support to upgrade * Updated small localization for teams plan to account for new starter plan * Removed invalid tests * Resolved issues around free trial flow for teams starter * Added new layout for teams starter free trial flow * Updated copy following demo. Resolved display issues discovered during demo * Removed temporary copy for testing * Updated the second step of free trial flow to use org display name * Updated invite user modal to display 10 instead of 20 as the invite limit for Teams Starter --------- Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
This commit is contained in:
parent
197059d4fa
commit
9f5226f8a6
@ -23,7 +23,10 @@
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
|
||||
<bit-hint>{{ "inviteMultipleEmailDesc" | i18n : "20" }}</bit-hint>
|
||||
<bit-hint>{{
|
||||
"inviteMultipleEmailDesc"
|
||||
| i18n : (organization.planProductType === ProductType.TeamsStarter ? "10" : "20")
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<fieldset role="radiogroup" aria-labelledby="roleGroupLabel" class="tw-mb-6">
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@ -37,7 +38,8 @@ import {
|
||||
} from "../../../shared/components/access-selector";
|
||||
|
||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||
import { freeOrgSeatLimitReachedValidator } from "./validators/free-org-inv-limit-reached.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,
|
||||
@ -180,11 +182,16 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
const emailsControlValidators = [
|
||||
Validators.required,
|
||||
commaSeparatedEmails,
|
||||
freeOrgSeatLimitReachedValidator(
|
||||
orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionFreePlan", organization.seats)
|
||||
),
|
||||
orgWithoutAdditionalSeatLimitReachedWithoutUpgradePathValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionFamiliesPlan", organization.seats)
|
||||
),
|
||||
];
|
||||
|
||||
const emailsControl = this.formGroup.get("emails");
|
||||
@ -367,10 +374,12 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
await this.userService.save(userView);
|
||||
} else {
|
||||
userView.id = this.params.organizationUserId;
|
||||
const maxEmailsCount =
|
||||
this.organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
|
||||
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
|
||||
if (emails.length > 20) {
|
||||
if (emails.length > maxEmailsCount) {
|
||||
this.formGroup.controls.emails.setErrors({
|
||||
tooManyEmails: { message: this.i18nService.t("tooManyEmails", 20) },
|
||||
tooManyEmails: { message: this.i18nService.t("tooManyEmails", maxEmailsCount) },
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -507,6 +516,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly ProductType = ProductType;
|
||||
}
|
||||
|
||||
function mapCollectionToAccessItemView(
|
||||
|
@ -4,7 +4,7 @@ import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
import { freeOrgSeatLimitReachedValidator } from "./free-org-inv-limit-reached.validator";
|
||||
import { orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator } from "./org-without-additional-seat-limit-reached-with-upgrade-path.validator";
|
||||
|
||||
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
Object.assign(
|
||||
@ -17,7 +17,7 @@ const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
props
|
||||
);
|
||||
|
||||
describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
let organization: Organization;
|
||||
let allOrganizationUserEmails: string[];
|
||||
let validatorFn: (control: AbstractControl) => ValidationErrors | null;
|
||||
@ -27,7 +27,7 @@ describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
});
|
||||
|
||||
it("should return null when control value is empty", () => {
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
@ -40,7 +40,7 @@ describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
});
|
||||
|
||||
it("should return null when control value is null", () => {
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
@ -57,7 +57,7 @@ describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
planProductType: ProductType.Free,
|
||||
seats: 2,
|
||||
});
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
@ -69,13 +69,40 @@ describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when max seats are not exceeded on teams starter plan", () => {
|
||||
organization = orgFactory({
|
||||
planProductType: ProductType.TeamsStarter,
|
||||
seats: 10,
|
||||
});
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 10 members without upgrading your plan."
|
||||
);
|
||||
const control = new FormControl(
|
||||
"user2@example.com," +
|
||||
"user3@example.com," +
|
||||
"user4@example.com," +
|
||||
"user5@example.com," +
|
||||
"user6@example.com," +
|
||||
"user7@example.com," +
|
||||
"user8@example.com," +
|
||||
"user9@example.com," +
|
||||
"user10@example.com"
|
||||
);
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return validation error when max seats are exceeded on free plan", () => {
|
||||
organization = orgFactory({
|
||||
planProductType: ProductType.Free,
|
||||
seats: 2,
|
||||
});
|
||||
const errorMessage = "You cannot invite more than 2 members without upgrading your plan.";
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
@ -93,7 +120,7 @@ describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
planProductType: ProductType.Enterprise,
|
||||
seats: 100,
|
||||
});
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
@ -4,13 +4,14 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
/**
|
||||
* Checks if the limit of free organization seats has been reached when adding new users
|
||||
* 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 freeOrgSeatLimitReachedValidator(
|
||||
export function orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
organization: Organization,
|
||||
allOrganizationUserEmails: string[],
|
||||
errorMessage: string
|
||||
@ -20,13 +21,20 @@ export function freeOrgSeatLimitReachedValidator(
|
||||
return null;
|
||||
}
|
||||
|
||||
const newEmailsToAdd = control.value
|
||||
.split(",")
|
||||
.filter(
|
||||
(newEmailToAdd: string) =>
|
||||
newEmailToAdd &&
|
||||
!allOrganizationUserEmails.some((existingEmail) => existingEmail === newEmailToAdd)
|
||||
);
|
||||
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.Free &&
|
||||
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
|
@ -0,0 +1,45 @@
|
||||
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;
|
||||
};
|
||||
}
|
@ -345,38 +345,85 @@ export class PeopleComponent
|
||||
);
|
||||
}
|
||||
|
||||
private async showFreeOrgUpgradeDialog(): Promise<void> {
|
||||
private getManageBillingText(): string {
|
||||
return this.organization.canEditSubscription ? "ManageBilling" : "NoManageBilling";
|
||||
}
|
||||
|
||||
private getProductKey(productType: ProductType): string {
|
||||
let product = "";
|
||||
switch (productType) {
|
||||
case ProductType.Free:
|
||||
product = "freeOrg";
|
||||
break;
|
||||
case ProductType.TeamsStarter:
|
||||
product = "teamsStarterPlan";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported product type: ${productType}`);
|
||||
}
|
||||
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),
|
||||
this.organization.seats
|
||||
);
|
||||
}
|
||||
|
||||
private getAcceptButtonText(): string {
|
||||
if (!this.organization.canEditSubscription) {
|
||||
return this.i18nService.t("ok");
|
||||
}
|
||||
|
||||
return this.i18nService.t(this.getDialogTitle(this.organization.planProductType));
|
||||
}
|
||||
|
||||
private async handleDialogClose(result: boolean | undefined): Promise<void> {
|
||||
if (!result || !this.organization.canEditSubscription) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async showSeatLimitReachedDialog(): Promise<void> {
|
||||
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
||||
title: this.i18nService.t("upgradeOrganization"),
|
||||
content: this.i18nService.t(
|
||||
this.organization.canEditSubscription
|
||||
? "freeOrgInvLimitReachedManageBilling"
|
||||
: "freeOrgInvLimitReachedNoManageBilling",
|
||||
this.organization.seats
|
||||
),
|
||||
content: this.getDialogContent(),
|
||||
type: "primary",
|
||||
acceptButtonText: this.getAcceptButtonText(),
|
||||
};
|
||||
|
||||
if (this.organization.canEditSubscription) {
|
||||
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
|
||||
} else {
|
||||
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
|
||||
orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn
|
||||
if (!this.organization.canEditSubscription) {
|
||||
orgUpgradeSimpleDialogOpts.cancelButtonText = null;
|
||||
}
|
||||
|
||||
const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts);
|
||||
|
||||
firstValueFrom(simpleDialog.closed).then((result: boolean | undefined) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && this.organization.canEditSubscription) {
|
||||
this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], {
|
||||
queryParams: { upgrade: true },
|
||||
});
|
||||
}
|
||||
});
|
||||
firstValueFrom(simpleDialog.closed).then(this.handleDialogClose.bind(this));
|
||||
}
|
||||
|
||||
async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) {
|
||||
@ -384,13 +431,14 @@ export class PeopleComponent
|
||||
// Click on user email: Edit Flow
|
||||
|
||||
// User attempting to invite new users in a free org with max users
|
||||
if (
|
||||
!user &&
|
||||
this.organization.planProductType === ProductType.Free &&
|
||||
this.allUsers.length === this.organization.seats
|
||||
) {
|
||||
if (!user && this.allUsers.length === this.organization.seats) {
|
||||
// Show org upgrade modal
|
||||
await this.showFreeOrgUpgradeDialog();
|
||||
if (
|
||||
this.organization.planProductType === ProductType.Free ||
|
||||
this.organization.planProductType === ProductType.TeamsStarter
|
||||
) {
|
||||
await this.showSeatLimitReachedDialog();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,9 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
} else if (qParams.plan === "teams") {
|
||||
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
|
||||
this.orgPlansComponent.product = ProductType.Teams;
|
||||
} else if (qParams.plan === "teamsStarter") {
|
||||
this.orgPlansComponent.plan = PlanType.TeamsStarter;
|
||||
this.orgPlansComponent.product = ProductType.TeamsStarter;
|
||||
} else if (qParams.plan === "enterprise") {
|
||||
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
|
||||
this.orgPlansComponent.product = ProductType.Enterprise;
|
||||
|
@ -0,0 +1,18 @@
|
||||
<h1 class="tw-text-4xl !tw-text-alt2">Begin Teams Starter Free Trial Now</h1>
|
||||
<div class="tw-pt-32">
|
||||
<h2 class="tw-text-2xl">
|
||||
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
|
||||
storage and sharing.
|
||||
</h2>
|
||||
</div>
|
||||
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
|
||||
<li>Powerful security for up to 10 users</li>
|
||||
<li>Collaborate and share securely</li>
|
||||
<li>Deploy and manage quickly and easily</li>
|
||||
<li>Access anywhere on any device</li>
|
||||
<li>Create your account to get started</li>
|
||||
</ul>
|
||||
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
|
||||
<app-logo-forbes></app-logo-forbes>
|
||||
<app-logo-us-news></app-logo-us-news>
|
||||
</div>
|
@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams3-content",
|
||||
templateUrl: "teams3-content.component.html",
|
||||
})
|
||||
export class Teams3ContentComponent {}
|
@ -28,6 +28,7 @@
|
||||
<app-teams-content *ngIf="layout === layouts.teams"></app-teams-content>
|
||||
<app-teams1-content *ngIf="layout === layouts.teams1"></app-teams1-content>
|
||||
<app-teams2-content *ngIf="layout === layouts.teams2"></app-teams2-content>
|
||||
<app-teams3-content *ngIf="layout === layouts.teams3"></app-teams3-content>
|
||||
<app-enterprise-content *ngIf="layout === layouts.enterprise"></app-enterprise-content>
|
||||
<app-enterprise1-content *ngIf="layout === layouts.enterprise1"></app-enterprise1-content>
|
||||
<app-enterprise2-content *ngIf="layout === layouts.enterprise2"></app-enterprise2-content>
|
||||
@ -60,7 +61,7 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
|
||||
<div class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100">
|
||||
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
||||
{{ "startYour7DayFreeTrialOfBitwardenFor" | i18n : org }}
|
||||
{{ "startYour7DayFreeTrialOfBitwardenFor" | i18n : orgDisplayName }}
|
||||
</h2>
|
||||
<environment-selector
|
||||
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
||||
|
@ -24,6 +24,7 @@ enum ValidOrgParams {
|
||||
families = "families",
|
||||
enterprise = "enterprise",
|
||||
teams = "teams",
|
||||
teamsStarter = "teamsStarter",
|
||||
individual = "individual",
|
||||
premium = "premium",
|
||||
free = "free",
|
||||
@ -34,6 +35,7 @@ enum ValidLayoutParams {
|
||||
teams = "teams",
|
||||
teams1 = "teams1",
|
||||
teams2 = "teams2",
|
||||
teams3 = "teams3",
|
||||
enterprise = "enterprise",
|
||||
enterprise1 = "enterprise1",
|
||||
enterprise2 = "enterprise2",
|
||||
@ -64,6 +66,7 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
trialFlowOrgs: string[] = [
|
||||
ValidOrgParams.teams,
|
||||
ValidOrgParams.teamsStarter,
|
||||
ValidOrgParams.enterprise,
|
||||
ValidOrgParams.families,
|
||||
];
|
||||
@ -143,6 +146,9 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
if (this.org === ValidOrgParams.families) {
|
||||
this.plan = PlanType.FamiliesAnnually;
|
||||
this.product = ProductType.Families;
|
||||
} else if (this.org === ValidOrgParams.teamsStarter) {
|
||||
this.plan = PlanType.TeamsStarter;
|
||||
this.product = ProductType.TeamsStarter;
|
||||
} else if (this.org === ValidOrgParams.teams) {
|
||||
this.plan = PlanType.TeamsAnnually;
|
||||
this.product = ProductType.Teams;
|
||||
@ -206,7 +212,9 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
// Set org info sub label
|
||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
||||
this.orgInfoSubLabel =
|
||||
"Enter your " + this.titleCasePipe.transform(this.org) + " organization information";
|
||||
"Enter your " +
|
||||
this.titleCasePipe.transform(this.orgDisplayName) +
|
||||
" organization information";
|
||||
} else if (event.previouslySelectedIndex === 1) {
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
||||
}
|
||||
@ -241,6 +249,14 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
this.verticalStepper.previous();
|
||||
}
|
||||
|
||||
get orgDisplayName() {
|
||||
if (this.org === "teamsStarter") {
|
||||
return "Teams Starter";
|
||||
}
|
||||
|
||||
return this.org;
|
||||
}
|
||||
|
||||
private setupFamilySponsorship(sponsorshipToken: string) {
|
||||
if (sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
|
@ -27,6 +27,7 @@ import { LogoUSNewsComponent } from "./content/logo-us-news.component";
|
||||
import { TeamsContentComponent } from "./content/teams-content.component";
|
||||
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
||||
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
||||
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
||||
|
||||
@ -55,6 +56,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
TeamsContentComponent,
|
||||
Teams1ContentComponent,
|
||||
Teams2ContentComponent,
|
||||
Teams3ContentComponent,
|
||||
CnetEnterpriseContentComponent,
|
||||
CnetIndividualContentComponent,
|
||||
CnetTeamsContentComponent,
|
||||
|
@ -24,7 +24,7 @@
|
||||
}}
|
||||
/{{ "yr" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectablePlan.SecretsManager">
|
||||
<ng-container *ngIf="!selectablePlan.PasswordManager && selectablePlan.SecretsManager">
|
||||
{{ "annual" | i18n }} -
|
||||
{{
|
||||
(selectablePlan.SecretsManager.basePrice === 0
|
||||
@ -46,7 +46,7 @@
|
||||
}}
|
||||
/{{ "monthAbbr" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectablePlan.SecretsManager">
|
||||
<ng-container *ngIf="!selectablePlan.PasswordManager && selectablePlan.SecretsManager">
|
||||
{{ "monthly" | i18n }} -
|
||||
{{
|
||||
(selectablePlan.SecretsManager.basePrice === 0
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { FormGroup } from "@angular/forms";
|
||||
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
import { OrganizationPlansComponent } from "../../organizations";
|
||||
@ -14,7 +15,8 @@ export class BillingComponent extends OrganizationPlansComponent {
|
||||
@Output() previousStep = new EventEmitter();
|
||||
|
||||
async ngOnInit() {
|
||||
const additionalSeats = this.product == ProductType.Families ? 0 : 1;
|
||||
const additionalSeats =
|
||||
this.product == ProductType.Families || this.plan === PlanType.TeamsStarter ? 0 : 1;
|
||||
this.formGroup.patchValue({
|
||||
name: this.orgInfoForm.value.name,
|
||||
billingEmail: this.orgInfoForm.value.email,
|
||||
|
@ -11,6 +11,7 @@
|
||||
[plan]="defaultUpgradePlan"
|
||||
[product]="defaultUpgradeProduct"
|
||||
[organizationId]="organizationId"
|
||||
[currentProductType]="currentProductType"
|
||||
(onCanceled)="cancel()"
|
||||
>
|
||||
</app-organization-plans>
|
||||
|
@ -10,6 +10,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
})
|
||||
export class ChangePlanComponent {
|
||||
@Input() organizationId: string;
|
||||
@Input() currentProductType: ProductType;
|
||||
@Output() onChanged = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
|
@ -52,9 +52,7 @@
|
||||
<label class="form-check-label" for="product{{ selectableProduct.product }}">
|
||||
{{ selectableProduct.nameLocalizationKey | i18n }}
|
||||
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : "1" }}</small>
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.product === productTypes.Enterprise; else fullFeatureList"
|
||||
>
|
||||
<ng-container *ngIf="selectableProduct.product === productTypes.Enterprise">
|
||||
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small>
|
||||
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small>
|
||||
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small>
|
||||
@ -66,6 +64,19 @@
|
||||
{{ "xDayFreeTrial" | i18n : selectableProduct.trialPeriodDays }}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
teamsStarterPlanFeatureFlagIsEnabled && selectableProduct.product === productTypes.Teams;
|
||||
else fullFeatureList
|
||||
"
|
||||
>
|
||||
<small>• {{ "includeAllTeamsStarterFeatures" | i18n }}</small>
|
||||
<small>• {{ "chooseMonthlyOrAnnualBilling" | i18n }}</small>
|
||||
<small>• {{ "abilityToAddMoreThanNMembers" | i18n : 10 }}</small>
|
||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||
• {{ "xDayFreeTrial" | i18n : selectableProduct.trialPeriodDays }}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-template #fullFeatureList>
|
||||
<small *ngIf="selectableProduct.product == productTypes.Free"
|
||||
>• {{ "limitedUsers" | i18n : selectableProduct.PasswordManager.maxSeats }}</small
|
||||
@ -73,6 +84,7 @@
|
||||
<small
|
||||
*ngIf="
|
||||
selectableProduct.product != productTypes.Free &&
|
||||
selectableProduct.product != productTypes.TeamsStarter &&
|
||||
selectableProduct.PasswordManager.maxSeats
|
||||
"
|
||||
>• {{ "addShareLimitedUsers" | i18n : selectableProduct.PasswordManager.maxSeats }}</small
|
||||
@ -118,15 +130,23 @@
|
||||
</ng-template>
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||
<ng-container *ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship">
|
||||
{{ selectableProduct.PasswordManager.basePrice / 12 | currency : "$" }} /{{
|
||||
"month" | i18n
|
||||
}},
|
||||
{{
|
||||
(selectableProduct.isAnnual
|
||||
? selectableProduct.PasswordManager.basePrice / 12
|
||||
: selectableProduct.PasswordManager.basePrice
|
||||
) | currency : "$"
|
||||
}}
|
||||
/{{ "month" | i18n }},
|
||||
{{ "includesXUsers" | i18n : selectableProduct.PasswordManager.baseSeats }}
|
||||
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
|
||||
{{ ("additionalUsers" | i18n).toLowerCase() }}
|
||||
{{ selectableProduct.PasswordManager.seatPrice / 12 | currency : "$" }} /{{
|
||||
"month" | i18n
|
||||
{{
|
||||
(selectableProduct.isAnnual
|
||||
? selectableProduct.PasswordManager.seatPrice / 12
|
||||
: selectableProduct.PasswordManager.seatPrice
|
||||
) | currency : "$"
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</span>
|
||||
@ -137,7 +157,13 @@
|
||||
"
|
||||
>
|
||||
{{
|
||||
"costPerUser" | i18n : (selectableProduct.PasswordManager.seatPrice / 12 | currency : "$")
|
||||
"costPerUser"
|
||||
| i18n
|
||||
: ((selectableProduct.isAnnual
|
||||
? selectableProduct.PasswordManager.seatPrice / 12
|
||||
: selectableProduct.PasswordManager.seatPrice
|
||||
)
|
||||
| currency : "$")
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</span>
|
||||
@ -249,7 +275,13 @@
|
||||
{{ "annually" | i18n }}
|
||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{ selectablePlan.PasswordManager.basePrice / 12 | currency : "$" }} × 12
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.basePrice / 12
|
||||
: selectablePlan.PasswordManager.basePrice
|
||||
) | currency : "$"
|
||||
}}
|
||||
× 12
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||
@ -269,8 +301,13 @@
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.seatPrice / 12 | currency : "$" }} × 12
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.seatPrice / 12
|
||||
: selectablePlan.PasswordManager.seatPrice
|
||||
) | currency : "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency : "$"
|
||||
@ -280,7 +317,12 @@
|
||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb / 12 | currency : "$" }}
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||
) | currency : "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency : "$" }} /{{ "year" | i18n }}
|
||||
</small>
|
||||
|
@ -58,6 +58,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
@Input() showFree = true;
|
||||
@Input() showCancel = false;
|
||||
@Input() acceptingSponsorship = false;
|
||||
@Input() currentProductType: ProductType;
|
||||
|
||||
@Input()
|
||||
get product(): ProductType {
|
||||
@ -196,39 +197,47 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get selectableProducts() {
|
||||
let validPlans = this.passwordManagerPlans.filter((plan) => plan.type !== PlanType.Custom);
|
||||
|
||||
if (this.formGroup.controls.businessOwned.value) {
|
||||
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
|
||||
}
|
||||
|
||||
if (!this.showFree) {
|
||||
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
|
||||
}
|
||||
|
||||
validPlans = validPlans.filter(
|
||||
(plan) =>
|
||||
!plan.legacyYear &&
|
||||
!plan.disabled &&
|
||||
(plan.isAnnual || plan.product === this.productTypes.Free)
|
||||
);
|
||||
|
||||
if (this.acceptingSponsorship) {
|
||||
const familyPlan = this.passwordManagerPlans.find(
|
||||
(plan) => plan.type === PlanType.FamiliesAnnually
|
||||
);
|
||||
this.discount = familyPlan.PasswordManager.basePrice;
|
||||
validPlans = [familyPlan];
|
||||
return [familyPlan];
|
||||
}
|
||||
|
||||
return validPlans;
|
||||
const businessOwnedIsChecked = this.formGroup.controls.businessOwned.value;
|
||||
|
||||
const result = this.passwordManagerPlans.filter(
|
||||
(plan) =>
|
||||
plan.type !== PlanType.Custom &&
|
||||
(!businessOwnedIsChecked || plan.canBeUsedByBusiness) &&
|
||||
(this.showFree || plan.product !== ProductType.Free) &&
|
||||
this.planIsEnabled(plan) &&
|
||||
(plan.isAnnual ||
|
||||
plan.product === ProductType.Free ||
|
||||
plan.product === ProductType.TeamsStarter) &&
|
||||
(this.currentProductType !== ProductType.TeamsStarter ||
|
||||
plan.product === ProductType.Teams ||
|
||||
plan.product === ProductType.Enterprise)
|
||||
);
|
||||
|
||||
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
get selectablePlans() {
|
||||
return this.passwordManagerPlans?.filter(
|
||||
(plan) =>
|
||||
!plan.legacyYear && !plan.disabled && plan.product === this.formGroup.controls.product.value
|
||||
const selectedProductType = this.formGroup.controls.product.value;
|
||||
const result = this.passwordManagerPlans?.filter(
|
||||
(plan) => this.planIsEnabled(plan) && plan.product === selectedProductType
|
||||
);
|
||||
|
||||
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder);
|
||||
return result;
|
||||
}
|
||||
|
||||
get teamsStarterPlanFeatureFlagIsEnabled(): boolean {
|
||||
return this.passwordManagerPlans.some((plan) => plan.product === ProductType.TeamsStarter);
|
||||
}
|
||||
|
||||
get hasProvider() {
|
||||
@ -392,8 +401,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
if (!this.formGroup.controls.businessOwned.value || this.selectedPlan.canBeUsedByBusiness) {
|
||||
return;
|
||||
}
|
||||
this.formGroup.controls.product.setValue(ProductType.Teams);
|
||||
this.formGroup.controls.plan.setValue(PlanType.TeamsAnnually);
|
||||
if (this.teamsStarterPlanFeatureFlagIsEnabled) {
|
||||
this.formGroup.controls.product.setValue(ProductType.TeamsStarter);
|
||||
this.formGroup.controls.plan.setValue(PlanType.TeamsStarter);
|
||||
} else {
|
||||
this.formGroup.controls.product.setValue(ProductType.Teams);
|
||||
this.formGroup.controls.plan.setValue(PlanType.TeamsAnnually);
|
||||
}
|
||||
this.changedProduct();
|
||||
}
|
||||
|
||||
@ -646,4 +660,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
this.changedProduct();
|
||||
}
|
||||
}
|
||||
|
||||
private planIsEnabled(plan: PlanResponse) {
|
||||
return !plan.disabled && !plan.legacyYear;
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@
|
||||
<ng-container *ngIf="subscription">
|
||||
<tr bitRow *ngFor="let i of subscriptionLineItems">
|
||||
<td bitCell [ngClass]="{ 'tw-pl-20': i.addonSubscriptionItem }">
|
||||
<span *ngIf="!i.addonSubscriptionItem">{{ i.productName }} -</span>
|
||||
<span *ngIf="!i.addonSubscriptionItem">{{ i.productName | i18n }} -</span>
|
||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||
{{ i.amount | currency : "$" }}
|
||||
</td>
|
||||
@ -127,6 +127,7 @@
|
||||
</button>
|
||||
<app-change-plan
|
||||
[organizationId]="organizationId"
|
||||
[currentProductType]="sub.plan.product"
|
||||
(onChanged)="closeChangePlan()"
|
||||
(onCanceled)="closeChangePlan()"
|
||||
*ngIf="showChangePlan"
|
||||
|
@ -11,6 +11,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@ -43,6 +44,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
private readonly _smBetaEndingDate = new Date(2023, 7, 15);
|
||||
private readonly _smGracePeriodEndingDate = new Date(2023, 10, 14);
|
||||
|
||||
protected readonly teamsStarter = ProductType.TeamsStarter;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@ -95,8 +98,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
const seatPriceTotal = this.sub.plan?.SecretsManager?.seatPrice * item.quantity;
|
||||
item.productName =
|
||||
itemTotalAmount === seatPriceTotal || item.name.includes("Service Accounts")
|
||||
? "SecretsManager"
|
||||
: "PasswordManager";
|
||||
? "secretsManager"
|
||||
: "passwordManager";
|
||||
return item;
|
||||
})
|
||||
.sort(sortSubscriptionItems);
|
||||
@ -234,6 +237,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
return (
|
||||
this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually2020 ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly2020 ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly2019
|
||||
);
|
||||
@ -253,6 +258,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
}
|
||||
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
|
||||
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
|
||||
} else if (this.userOrg.planProductType === ProductType.TeamsStarter) {
|
||||
return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10);
|
||||
} else if (this.sub.maxAutoscaleSeats == null) {
|
||||
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
|
||||
} else {
|
||||
|
@ -83,6 +83,7 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy {
|
||||
case ProductType.Free:
|
||||
return this.i18nService.t("free2PersonOrganization");
|
||||
case ProductType.Teams:
|
||||
case ProductType.TeamsStarter:
|
||||
return this.i18nService.t("planNameTeams");
|
||||
case ProductType.Enterprise:
|
||||
return this.i18nService.t("planNameEnterprise");
|
||||
|
@ -2280,6 +2280,9 @@
|
||||
"contactSupport": {
|
||||
"message": "Contact customer support"
|
||||
},
|
||||
"contactSupportShort": {
|
||||
"message": "Contact Support"
|
||||
},
|
||||
"updatedPaymentMethod": {
|
||||
"message": "Updated payment method."
|
||||
},
|
||||
@ -2381,6 +2384,9 @@
|
||||
"planDescTeams": {
|
||||
"message": "For businesses and other team organizations."
|
||||
},
|
||||
"planNameTeamsStarter": {
|
||||
"message": "Teams Starter"
|
||||
},
|
||||
"planNameEnterprise": {
|
||||
"message": "Enterprise"
|
||||
},
|
||||
@ -3485,6 +3491,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"subscriptionUserSeatsWithoutAdditionalSeatsOption": {
|
||||
"message": "You can invite up to $COUNT$ members for no additional charge. Contact Customer Support to upgrade your plan and invite more members.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subscriptionFreePlan": {
|
||||
"message": "You cannot invite more than $COUNT$ members without upgrading your plan.",
|
||||
"placeholders": {
|
||||
@ -3983,6 +3998,21 @@
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "All Teams features, plus:"
|
||||
},
|
||||
"includeAllTeamsStarterFeatures": {
|
||||
"message": "All Teams Starter features, plus:"
|
||||
},
|
||||
"chooseMonthlyOrAnnualBilling": {
|
||||
"message": "Choose monthly or annual billing"
|
||||
},
|
||||
"abilityToAddMoreThanNMembers": {
|
||||
"message": "Ability to add more than $COUNT$ members",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "SSO Authentication via SAML2.0 and OpenID Connect"
|
||||
},
|
||||
@ -6598,6 +6628,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"teamsStarterPlanInvLimitReachedManageBilling": {
|
||||
"message": "Teams Starter plans may have up to $SEATCOUNT$ members. Contact Customer Support to upgrade your plan and invite more members.",
|
||||
"placeholders": {
|
||||
"seatcount": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"teamsStarterPlanInvLimitReachedNoManageBilling": {
|
||||
"message": "Teams Starter plans may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade your plan and invite more members.",
|
||||
"placeholders": {
|
||||
"seatcount": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"freeOrgMaxCollectionReachedManageBilling": {
|
||||
"message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Upgrade to a paid plan to add more collections.",
|
||||
"placeholders": {
|
||||
|
@ -7,8 +7,13 @@ export enum PlanType {
|
||||
EnterpriseAnnually2019 = 5,
|
||||
Custom = 6,
|
||||
FamiliesAnnually = 7,
|
||||
TeamsMonthly = 8,
|
||||
TeamsAnnually = 9,
|
||||
EnterpriseMonthly = 10,
|
||||
EnterpriseAnnually = 11,
|
||||
TeamsMonthly2020 = 8,
|
||||
TeamsAnnually2020 = 9,
|
||||
EnterpriseMonthly2020 = 10,
|
||||
EnterpriseAnnually2020 = 11,
|
||||
TeamsMonthly = 12,
|
||||
TeamsAnnually = 13,
|
||||
EnterpriseMonthly = 14,
|
||||
EnterpriseAnnually = 15,
|
||||
TeamsStarter = 16,
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export class PlanResponse extends BaseResponse {
|
||||
this.hasResetPassword = this.getResponseProperty("HasResetPassword");
|
||||
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
||||
this.upgradeSortOrder = this.getResponseProperty("UpgradeSortOrder");
|
||||
this.displaySortOrder = this.getResponseProperty("SortOrder");
|
||||
this.displaySortOrder = this.getResponseProperty("DisplaySortOrder");
|
||||
this.legacyYear = this.getResponseProperty("LegacyYear");
|
||||
this.disabled = this.getResponseProperty("Disabled");
|
||||
const passwordManager = this.getResponseProperty("PasswordManager");
|
||||
|
@ -3,4 +3,5 @@ export enum ProductType {
|
||||
Families = 1,
|
||||
Teams = 2,
|
||||
Enterprise = 3,
|
||||
TeamsStarter = 4,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user