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)!;
}
}