mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-16664] Incorrect price value in "add new organization" modal (#12710)
This commit is contained in:
@@ -9298,6 +9298,9 @@
|
|||||||
"monthPerMember": {
|
"monthPerMember": {
|
||||||
"message": "month per member"
|
"message": "month per member"
|
||||||
},
|
},
|
||||||
|
"monthPerMemberBilledAnnually": {
|
||||||
|
"message": "month per member billed annually"
|
||||||
|
},
|
||||||
"seats": {
|
"seats": {
|
||||||
"message": "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 class="tw-grid tw-grid-flow-col tw-auto-cols-[minmax(0,_2fr)] tw-gap-4 tw-mb-4">
|
||||||
<div
|
<div
|
||||||
*ngFor="let planCard of planCards"
|
*ngFor="let planCard of planCards"
|
||||||
[ngClass]="getPlanCardContainerClasses(planCard.selected)"
|
[ngClass]="planCard.getContainerClasses()"
|
||||||
(click)="selectPlan(planCard.name)"
|
(click)="selectPlan(planCard.name)"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -29,9 +29,11 @@
|
|||||||
<div class="tw-pl-5 tw-py-4 tw-pr-4" [ngClass]="{ 'tw-pt-10': !planCard.selected }">
|
<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>
|
<h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3>
|
||||||
<span class="tw-text-2xl tw-font-semibold">{{
|
<span class="tw-text-2xl tw-font-semibold">{{
|
||||||
planCard.cost | currency: "$"
|
planCard.getMonthlyCost() | currency: "$"
|
||||||
}}</span>
|
}}</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,8 +47,8 @@
|
|||||||
<input type="text" bitInput formControlName="organizationName" />
|
<input type="text" bitInput formControlName="organizationName" />
|
||||||
<bit-error-summary
|
<bit-error-summary
|
||||||
*ngIf="
|
*ngIf="
|
||||||
formGroup.get('organizationName').errors?.['maxLength'] &&
|
formGroup.controls.organizationName.errors?.['maxLength'] &&
|
||||||
formGroup.get('organizationName').touched
|
formGroup.controls.organizationName.touched
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ "organizationNameMaxLength" | i18n }}
|
{{ "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 { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { BasePortalOutlet } from "@angular/cdk/portal";
|
||||||
import { Component, Inject, OnInit } from "@angular/core";
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
|
|
||||||
@@ -25,48 +24,42 @@ export enum CreateClientDialogResultType {
|
|||||||
|
|
||||||
export const openCreateClientDialog = (
|
export const openCreateClientDialog = (
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
dialogConfig: DialogConfig<CreateClientDialogParams>,
|
dialogConfig: DialogConfig<
|
||||||
|
CreateClientDialogParams,
|
||||||
|
DialogRef<CreateClientDialogResultType, unknown>,
|
||||||
|
BasePortalOutlet
|
||||||
|
>,
|
||||||
) =>
|
) =>
|
||||||
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
|
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
|
||||||
CreateClientDialogComponent,
|
CreateClientDialogComponent,
|
||||||
dialogConfig,
|
dialogConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
type PlanCard = {
|
export class PlanCard {
|
||||||
name: string;
|
readonly name: string;
|
||||||
cost: number;
|
private readonly cost: number;
|
||||||
type: PlanType;
|
readonly type: PlanType;
|
||||||
plan: PlanResponse;
|
readonly plan: PlanResponse;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
constructor(name: string, cost: number, type: PlanType, plan: PlanResponse, selected: boolean) {
|
||||||
templateUrl: "./create-client-dialog.component.html",
|
this.name = name;
|
||||||
})
|
this.cost = cost;
|
||||||
export class CreateClientDialogComponent implements OnInit {
|
this.type = type;
|
||||||
protected discountPercentage: number;
|
this.plan = plan;
|
||||||
protected formGroup = new FormGroup({
|
this.selected = selected;
|
||||||
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;
|
|
||||||
|
|
||||||
private providerPlans: ProviderPlanResponse[];
|
getMonthlyCost(): number {
|
||||||
|
return this.plan.isAnnual ? this.cost / 12 : this.cost;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
getTimePerMemberLabel(): string {
|
||||||
private billingApiService: BillingApiServiceAbstraction,
|
return this.plan.isAnnual ? "monthPerMemberBilledAnnually" : "monthPerMember";
|
||||||
@Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams,
|
}
|
||||||
private dialogRef: DialogRef<CreateClientDialogResultType>,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private toastService: ToastService,
|
|
||||||
private webProviderService: WebProviderService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
protected getPlanCardContainerClasses(selected: boolean) {
|
getContainerClasses() {
|
||||||
switch (selected) {
|
switch (this.selected) {
|
||||||
case true: {
|
case true: {
|
||||||
return [
|
return [
|
||||||
"tw-group/plan-card-container",
|
"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> {
|
async ngOnInit(): Promise<void> {
|
||||||
const response = await this.billingApiService.getProviderSubscription(
|
const response = await this.billingApiService.getProviderSubscription(
|
||||||
@@ -114,6 +142,10 @@ export class CreateClientDialogComponent implements OnInit {
|
|||||||
const providerPlan = this.providerPlans[i];
|
const providerPlan = this.providerPlans[i];
|
||||||
const plan = this.dialogParams.plans.find((plan) => plan.type === providerPlan.type);
|
const plan = this.dialogParams.plans.find((plan) => plan.type === providerPlan.type);
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let planName: string;
|
let planName: string;
|
||||||
switch (plan.productTier) {
|
switch (plan.productTier) {
|
||||||
case ProductTierType.Teams: {
|
case ProductTierType.Teams: {
|
||||||
@@ -124,23 +156,28 @@ export class CreateClientDialogComponent implements OnInit {
|
|||||||
planName = this.i18nService.t("planNameEnterprise");
|
planName = this.i18nService.t("planNameEnterprise");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.planCards.push({
|
this.planCards.push(
|
||||||
name: planName,
|
new PlanCard(
|
||||||
cost: plan.PasswordManager.providerPortalSeatPrice * discountFactor,
|
planName,
|
||||||
type: plan.type,
|
plan.PasswordManager.providerPortalSeatPrice * discountFactor,
|
||||||
plan: plan,
|
plan.type,
|
||||||
selected: i === 0,
|
plan,
|
||||||
});
|
i === 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected selectPlan(name: string) {
|
protected selectPlan(name: string) {
|
||||||
this.planCards.find((planCard) => planCard.name === name).selected = true;
|
this.planCards.forEach((planCard) => {
|
||||||
this.planCards.find((planCard) => planCard.name !== name).selected = false;
|
planCard.selected = planCard.name === name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
@@ -152,17 +189,21 @@ export class CreateClientDialogComponent implements OnInit {
|
|||||||
|
|
||||||
const selectedPlanCard = this.planCards.find((planCard) => planCard.selected);
|
const selectedPlanCard = this.planCards.find((planCard) => planCard.selected);
|
||||||
|
|
||||||
|
if (!selectedPlanCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.webProviderService.createClientOrganization(
|
await this.webProviderService.createClientOrganization(
|
||||||
this.dialogParams.providerId,
|
this.dialogParams.providerId,
|
||||||
this.formGroup.value.organizationName,
|
this.formGroup.controls.organizationName.value,
|
||||||
this.formGroup.value.clientOwnerEmail,
|
this.formGroup.controls.clientOwnerEmail.value,
|
||||||
selectedPlanCard.type,
|
selectedPlanCard.type,
|
||||||
this.formGroup.value.seats,
|
this.formGroup.controls.seats.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
title: "",
|
||||||
message: this.i18nService.t("createdNewClient"),
|
message: this.i18nService.t("createdNewClient"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,7 +219,7 @@ export class CreateClientDialogComponent implements OnInit {
|
|||||||
|
|
||||||
const openSeats = selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats;
|
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;
|
return unassignedSeats > 0 ? unassignedSeats : 0;
|
||||||
}
|
}
|
||||||
@@ -191,22 +232,22 @@ export class CreateClientDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedProviderPlan.purchasedSeats > 0) {
|
if (selectedProviderPlan.purchasedSeats > 0) {
|
||||||
return this.formGroup.value.seats;
|
return this.formGroup.controls.seats.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalSeatsPurchased =
|
const additionalSeatsPurchased =
|
||||||
this.formGroup.value.seats +
|
this.formGroup.controls.seats.value +
|
||||||
selectedProviderPlan.assignedSeats -
|
selectedProviderPlan.assignedSeats -
|
||||||
selectedProviderPlan.seatMinimum;
|
selectedProviderPlan.seatMinimum;
|
||||||
|
|
||||||
return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0;
|
return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSelectedProviderPlan(): ProviderPlanResponse {
|
private getSelectedProviderPlan(): ProviderPlanResponse | null {
|
||||||
if (this.loading || !this.planCards) {
|
if (this.loading || !this.planCards) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan;
|
const selectedPlan = this.planCards.find((planCard) => planCard.selected)!.plan;
|
||||||
return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name);
|
return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user