From 9f5226f8a6e71f4aaa6348306c912b238841dc8e Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:32:44 -0400 Subject: [PATCH] [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> --- .../member-dialog.component.html | 5 +- .../member-dialog/member-dialog.component.ts | 19 ++- ...ached-with-upgrade-path.validator.spec.ts} | 41 +++++-- ...it-reached-with-upgrade-path.validator.ts} | 26 +++-- ...-reached-without-upgrade-path.validator.ts | 45 ++++++++ .../organizations/members/people.component.ts | 108 +++++++++++++----- .../settings/create-organization.component.ts | 3 + .../content/teams3-content.component.html | 18 +++ .../content/teams3-content.component.ts | 7 ++ .../trial-initiation.component.html | 3 +- .../trial-initiation.component.ts | 18 ++- .../trial-initiation.module.ts | 2 + .../trial-initiation/billing.component.html | 4 +- .../trial-initiation/billing.component.ts | 4 +- .../organizations/change-plan.component.html | 1 + .../organizations/change-plan.component.ts | 1 + .../organization-plans.component.html | 68 ++++++++--- .../organization-plans.component.ts | 66 +++++++---- ...nization-subscription-cloud.component.html | 3 +- ...ganization-subscription-cloud.component.ts | 11 +- .../billing/shared/sm-subscribe.component.ts | 1 + apps/web/src/locales/en/messages.json | 48 ++++++++ .../src/billing/enums/plan-type.enum.ts | 13 ++- .../billing/models/response/plan.response.ts | 2 +- libs/common/src/enums/product-type.enum.ts | 1 + 25 files changed, 417 insertions(+), 101 deletions(-) rename apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/{free-org-inv-limit-reached.validator.spec.ts => org-without-additional-seat-limit-reached-with-upgrade-path.validator.spec.ts} (66%) rename apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/{free-org-inv-limit-reached.validator.ts => org-without-additional-seat-limit-reached-with-upgrade-path.validator.ts} (63%) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-without-upgrade-path.validator.ts create mode 100644 apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html create mode 100644 apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 34d407e7b2..1c718372af 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -23,7 +23,10 @@ {{ "email" | i18n }} - {{ "inviteMultipleEmailDesc" | i18n : "20" }} + {{ + "inviteMultipleEmailDesc" + | i18n : (organization.planProductType === ProductType.TeamsStarter ? "10" : "20") + }}
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index f885a05453..9d0dc6799d 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -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( diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/free-org-inv-limit-reached.validator.spec.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator.spec.ts similarity index 66% rename from apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/free-org-inv-limit-reached.validator.spec.ts rename to apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator.spec.ts index 5c17a128ac..09ba1d583d 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/free-org-inv-limit-reached.validator.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator.spec.ts @@ -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 = {}) => Object.assign( @@ -17,7 +17,7 @@ const orgFactory = (props: Partial = {}) => 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." diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/free-org-inv-limit-reached.validator.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator.ts similarity index 63% rename from apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/free-org-inv-limit-reached.validator.ts rename to apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator.ts index 6d5c45a64d..76634e7196 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/free-org-inv-limit-reached.validator.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator.ts @@ -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 diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-without-upgrade-path.validator.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-without-upgrade-path.validator.ts new file mode 100644 index 0000000000..f59d35d637 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-without-additional-seat-limit-reached-without-upgrade-path.validator.ts @@ -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; + }; +} diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index a7d4c00d2b..896d88090f 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -345,38 +345,85 @@ export class PeopleComponent ); } - private async showFreeOrgUpgradeDialog(): Promise { + 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 { + 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 { 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; } diff --git a/apps/web/src/app/admin-console/settings/create-organization.component.ts b/apps/web/src/app/admin-console/settings/create-organization.component.ts index 7728ddc540..5e8aaf8684 100644 --- a/apps/web/src/app/admin-console/settings/create-organization.component.ts +++ b/apps/web/src/app/admin-console/settings/create-organization.component.ts @@ -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; diff --git a/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html b/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html new file mode 100644 index 0000000000..6354bc7c0f --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html @@ -0,0 +1,18 @@ +

Begin Teams Starter Free Trial Now

+
+

+ Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password + storage and sharing. +

+
+
    +
  • Powerful security for up to 10 users
  • +
  • Collaborate and share securely
  • +
  • Deploy and manage quickly and easily
  • +
  • Access anywhere on any device
  • +
  • Create your account to get started
  • +
+
+ + +
diff --git a/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts b/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts new file mode 100644 index 0000000000..df91268ab2 --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-teams3-content", + templateUrl: "teams3-content.component.html", +}) +export class Teams3ContentComponent {} diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html index af8c255f63..1e2f395085 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html @@ -28,6 +28,7 @@ + @@ -60,7 +61,7 @@

- {{ "startYour7DayFreeTrialOfBitwardenFor" | i18n : org }} + {{ "startYour7DayFreeTrialOfBitwardenFor" | i18n : orgDisplayName }}

- + {{ "annual" | i18n }} - {{ (selectablePlan.SecretsManager.basePrice === 0 @@ -46,7 +46,7 @@ }} /{{ "monthAbbr" | i18n }} - + {{ "monthly" | i18n }} - {{ (selectablePlan.SecretsManager.basePrice === 0 diff --git a/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts index 2e54019165..30ed2c9ddd 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts @@ -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, diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html index 0861c51643..27ca56c2d4 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.html +++ b/apps/web/src/app/billing/organizations/change-plan.component.html @@ -11,6 +11,7 @@ [plan]="defaultUpgradePlan" [product]="defaultUpgradeProduct" [organizationId]="organizationId" + [currentProductType]="currentProductType" (onCanceled)="cancel()" > diff --git a/apps/web/src/app/billing/organizations/change-plan.component.ts b/apps/web/src/app/billing/organizations/change-plan.component.ts index d14fd96a97..10ac04b418 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan.component.ts @@ -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(); diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index 6dd97b1eec..ff0458c39a 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -52,9 +52,7 @@