mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-16664] Incorrect price value in "add new organization" modal (#12710)
This commit is contained in:
@@ -9298,6 +9298,9 @@
|
||||
"monthPerMember": {
|
||||
"message": "month per member"
|
||||
},
|
||||
"monthPerMemberBilledAnnually": {
|
||||
"message": "month per member billed annually"
|
||||
},
|
||||
"seats": {
|
||||
"message": "Seats"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="tw-grid tw-grid-flow-col tw-auto-cols-[minmax(0,_2fr)] tw-gap-4 tw-mb-4">
|
||||
<div
|
||||
*ngFor="let planCard of planCards"
|
||||
[ngClass]="getPlanCardContainerClasses(planCard.selected)"
|
||||
[ngClass]="planCard.getContainerClasses()"
|
||||
(click)="selectPlan(planCard.name)"
|
||||
tabindex="0"
|
||||
>
|
||||
@@ -29,9 +29,11 @@
|
||||
<div class="tw-pl-5 tw-py-4 tw-pr-4" [ngClass]="{ 'tw-pt-10': !planCard.selected }">
|
||||
<h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3>
|
||||
<span class="tw-text-2xl tw-font-semibold">{{
|
||||
planCard.cost | currency: "$"
|
||||
planCard.getMonthlyCost() | currency: "$"
|
||||
}}</span>
|
||||
<span class="tw-text-sm tw-font-bold">/ {{ "monthPerMember" | i18n }}</span>
|
||||
<span class="tw-text-sm tw-font-bold"
|
||||
>/ {{ planCard.getTimePerMemberLabel() | i18n }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,8 +47,8 @@
|
||||
<input type="text" bitInput formControlName="organizationName" />
|
||||
<bit-error-summary
|
||||
*ngIf="
|
||||
formGroup.get('organizationName').errors?.['maxLength'] &&
|
||||
formGroup.get('organizationName').touched
|
||||
formGroup.controls.organizationName.errors?.['maxLength'] &&
|
||||
formGroup.controls.organizationName.touched
|
||||
"
|
||||
>
|
||||
{{ "organizationNameMaxLength" | i18n }}
|
||||
|
||||
@@ -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<CreateClientDialogParams>,
|
||||
dialogConfig: DialogConfig<
|
||||
CreateClientDialogParams,
|
||||
DialogRef<CreateClientDialogResultType, unknown>,
|
||||
BasePortalOutlet
|
||||
>,
|
||||
) =>
|
||||
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
|
||||
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<string>("", [Validators.required, Validators.email]),
|
||||
organizationName: new FormControl<string>("", [Validators.required, Validators.maxLength(50)]),
|
||||
seats: new FormControl<number>(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<CreateClientDialogResultType>,
|
||||
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<string>("", {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.email],
|
||||
}),
|
||||
organizationName: new FormControl<string>("", {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.maxLength(50)],
|
||||
}),
|
||||
seats: new FormControl<number>(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<CreateClientDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
private webProviderService: WebProviderService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
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)!;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user