diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 95378c90c97..0a4c91b6f10 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9298,6 +9298,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index 78f2cb41bef..c11b23db9fb 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -15,7 +15,7 @@
@@ -29,9 +29,11 @@

{{ planCard.name }}

{{ - planCard.cost | currency: "$" + planCard.getMonthlyCost() | currency: "$" }} - / {{ "monthPerMember" | i18n }} + / {{ planCard.getTimePerMemberLabel() | i18n }}
@@ -45,8 +47,8 @@

{{ planCard.name }}

{{ "organizationNameMaxLength" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index 2a27b1b32f3..38eaed83937 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -1,6 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { BasePortalOutlet } from "@angular/cdk/portal"; import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; @@ -25,48 +24,42 @@ export enum CreateClientDialogResultType { export const openCreateClientDialog = ( dialogService: DialogService, - dialogConfig: DialogConfig, + dialogConfig: DialogConfig< + CreateClientDialogParams, + DialogRef, + BasePortalOutlet + >, ) => dialogService.open( CreateClientDialogComponent, dialogConfig, ); -type PlanCard = { - name: string; - cost: number; - type: PlanType; - plan: PlanResponse; +export class PlanCard { + readonly name: string; + private readonly cost: number; + readonly type: PlanType; + readonly plan: PlanResponse; selected: boolean; -}; -@Component({ - templateUrl: "./create-client-dialog.component.html", -}) -export class CreateClientDialogComponent implements OnInit { - protected discountPercentage: number; - protected formGroup = new FormGroup({ - clientOwnerEmail: new FormControl("", [Validators.required, Validators.email]), - organizationName: new FormControl("", [Validators.required, Validators.maxLength(50)]), - seats: new FormControl(null, [Validators.required, Validators.min(1)]), - }); - protected loading = true; - protected planCards: PlanCard[]; - protected ResultType = CreateClientDialogResultType; + constructor(name: string, cost: number, type: PlanType, plan: PlanResponse, selected: boolean) { + this.name = name; + this.cost = cost; + this.type = type; + this.plan = plan; + this.selected = selected; + } - private providerPlans: ProviderPlanResponse[]; + getMonthlyCost(): number { + return this.plan.isAnnual ? this.cost / 12 : this.cost; + } - constructor( - private billingApiService: BillingApiServiceAbstraction, - @Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - private webProviderService: WebProviderService, - ) {} + getTimePerMemberLabel(): string { + return this.plan.isAnnual ? "monthPerMemberBilledAnnually" : "monthPerMember"; + } - protected getPlanCardContainerClasses(selected: boolean) { - switch (selected) { + getContainerClasses() { + switch (this.selected) { case true: { return [ "tw-group/plan-card-container", @@ -97,6 +90,41 @@ export class CreateClientDialogComponent implements OnInit { } } } +} + +@Component({ + templateUrl: "./create-client-dialog.component.html", +}) +export class CreateClientDialogComponent implements OnInit { + protected discountPercentage: number | null | undefined; + protected formGroup = new FormGroup({ + clientOwnerEmail: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.email], + }), + organizationName: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(50)], + }), + seats: new FormControl(1, { + nonNullable: true, + validators: [Validators.required, Validators.min(1)], + }), + }); + protected loading = true; + protected planCards: PlanCard[] = []; + protected ResultType = CreateClientDialogResultType; + + private providerPlans: ProviderPlanResponse[] = []; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + @Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private toastService: ToastService, + private webProviderService: WebProviderService, + ) {} async ngOnInit(): Promise { const response = await this.billingApiService.getProviderSubscription( @@ -114,6 +142,10 @@ export class CreateClientDialogComponent implements OnInit { const providerPlan = this.providerPlans[i]; const plan = this.dialogParams.plans.find((plan) => plan.type === providerPlan.type); + if (!plan) { + continue; + } + let planName: string; switch (plan.productTier) { case ProductTierType.Teams: { @@ -124,23 +156,28 @@ export class CreateClientDialogComponent implements OnInit { planName = this.i18nService.t("planNameEnterprise"); break; } + default: + continue; } - this.planCards.push({ - name: planName, - cost: plan.PasswordManager.providerPortalSeatPrice * discountFactor, - type: plan.type, - plan: plan, - selected: i === 0, - }); + this.planCards.push( + new PlanCard( + planName, + plan.PasswordManager.providerPortalSeatPrice * discountFactor, + plan.type, + plan, + i === 0, + ), + ); } this.loading = false; } protected selectPlan(name: string) { - this.planCards.find((planCard) => planCard.name === name).selected = true; - this.planCards.find((planCard) => planCard.name !== name).selected = false; + this.planCards.forEach((planCard) => { + planCard.selected = planCard.name === name; + }); } submit = async () => { @@ -152,17 +189,21 @@ export class CreateClientDialogComponent implements OnInit { const selectedPlanCard = this.planCards.find((planCard) => planCard.selected); + if (!selectedPlanCard) { + return; + } + await this.webProviderService.createClientOrganization( this.dialogParams.providerId, - this.formGroup.value.organizationName, - this.formGroup.value.clientOwnerEmail, + this.formGroup.controls.organizationName.value, + this.formGroup.controls.clientOwnerEmail.value, selectedPlanCard.type, - this.formGroup.value.seats, + this.formGroup.controls.seats.value, ); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("createdNewClient"), }); @@ -178,7 +219,7 @@ export class CreateClientDialogComponent implements OnInit { const openSeats = selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats; - const unassignedSeats = openSeats - this.formGroup.value.seats; + const unassignedSeats = openSeats - this.formGroup.controls.seats.value; return unassignedSeats > 0 ? unassignedSeats : 0; } @@ -191,22 +232,22 @@ export class CreateClientDialogComponent implements OnInit { } if (selectedProviderPlan.purchasedSeats > 0) { - return this.formGroup.value.seats; + return this.formGroup.controls.seats.value; } const additionalSeatsPurchased = - this.formGroup.value.seats + + this.formGroup.controls.seats.value + selectedProviderPlan.assignedSeats - selectedProviderPlan.seatMinimum; return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0; } - private getSelectedProviderPlan(): ProviderPlanResponse { + private getSelectedProviderPlan(): ProviderPlanResponse | null { if (this.loading || !this.planCards) { return null; } - const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan; - return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name); + const selectedPlan = this.planCards.find((planCard) => planCard.selected)!.plan; + return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name)!; } }