diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 91e2e83a92c..3f0c31d56cf 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -365,621 +365,11 @@ (taxInformationChanged)="taxInformationChanged($event)" > -
-

- {{ "total" | i18n }}: - {{ total - calculateTotalAppliedDiscount(total) | currency: "USD" : "$" }} USD - / {{ selectedPlanInterval | i18n }} - -

-
- -
- -

- {{ "passwordManager" | i18n }} -

-

- - {{ passwordManagerSeats }} - {{ "members" | i18n }} × - {{ - (selectedPlan.isAnnual - ? selectedPlan.PasswordManager.basePrice / 12 - : selectedPlan.PasswordManager.basePrice - ) | currency: "$" - }} - /{{ selectedPlanInterval | i18n }} - - - - {{ - selectedPlan.PasswordManager.basePrice | currency: "$" - }} - {{ "freeWithSponsorship" | i18n }} - - - {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} - - -

-

- - {{ "additionalUsers" | i18n }}: - {{ passwordManagerSeats || 0 }}  - {{ "members" | i18n }} - × - {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - - {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} - -

-

- - {{ storageGb }} - {{ "additionalStorageGbMessage" | i18n }} - × - {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - {{ additionalStorageTotal(selectedPlan) | currency: "$" }} -

- -

- - - {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} - - {{ - calculateTotalAppliedDiscount( - passwordManagerSeatTotal(selectedPlan) + additionalStorageTotal(selectedPlan) - ) | currency: "$" - }} - -

- -

- {{ "secretsManager" | i18n }} -

-

- - {{ sub?.smSeats }} - {{ "members" | i18n }} × - {{ - (selectedPlan.isAnnual - ? selectedPlan.SecretsManager.basePrice / 12 - : selectedPlan.SecretsManager.basePrice - ) | currency: "$" - }} - /{{ selectedPlanInterval | i18n }} - -

-

- - {{ "additionalUsers" | i18n }}: - {{ sub?.smSeats || 0 }}  - {{ "members" | i18n }} - × - {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - - {{ secretsManagerSeatTotal(selectedPlan, sub.smSeats) | currency: "$" }} - -

-

- - {{ additionalServiceAccount }} - {{ "serviceAccounts" | i18n | lowercase }} - × - {{ selectedPlan?.SecretsManager?.additionalPricePerServiceAccount | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} -

- -

- - - {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} - - {{ - calculateTotalAppliedDiscount( - additionalServiceAccountTotal(selectedPlan) + - secretsManagerSeatTotal(selectedPlan, sub.smSeats) - ) | currency: "$" - }} - -

-
- -

- {{ "passwordManager" | i18n }} -

-

- - {{ "basePrice" | i18n }}: - {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} - {{ "monthAbbr" | i18n }} - - - {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} - -

-

- - {{ "additionalUsers" | i18n }}: - {{ passwordManagerSeats }}  - {{ "members" | i18n }} - × - {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} - -

-

- - {{ storageGb }} - {{ "additionalStorageGbMessage" | i18n }} - × - {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - {{ - storageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" - }} -

- -

- - - {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} - - {{ calculateTotalAppliedDiscount(total) | currency: "$" }} - -

- -

- {{ "secretsManager" | i18n }} -

-

- - {{ "basePrice" | i18n }}: - {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} - {{ "monthAbbr" | i18n }} - - - {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} - -

-

- - {{ "additionalUsers" | i18n }}: - {{ sub?.smSeats }}  - {{ "members" | i18n }} - × - {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - {{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} - -

-

- - {{ additionalServiceAccount }} - {{ "serviceAccounts" | i18n | lowercase }} - × - {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} -

- -

- - - {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} - - {{ - additionalServiceAccountTotal(selectedPlan) + - secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" - }} - -

-
-
- -
- - -

- {{ "secretsManager" | i18n }} -

-

- - {{ sub?.smSeats }} - {{ "members" | i18n }} × - {{ - (selectedPlan.isAnnual - ? selectedPlan.SecretsManager.basePrice / 12 - : selectedPlan.SecretsManager.basePrice - ) | currency: "$" - }} - /{{ selectedPlanInterval | i18n }} - -

-

- - {{ "additionalUsers" | i18n }}: - {{ sub?.smSeats || 0 }}  - {{ "members" | i18n }} - × - {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - - {{ secretsManagerSeatTotal(selectedPlan, sub.smSeats) | currency: "$" }} - -

-

- - {{ additionalServiceAccount }} - {{ "serviceAccounts" | i18n }} - × - {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} -

- -

- {{ "passwordManager" | i18n }} -

-

- - {{ sub?.seats }} - {{ "members" | i18n }} × - {{ - (selectedPlan.isAnnual - ? selectedPlan.PasswordManager.basePrice / 12 - : selectedPlan.PasswordManager.basePrice - ) | currency: "$" - }} - /{{ selectedPlanInterval | i18n }} - - - - {{ - selectedPlan.PasswordManager.basePrice | currency: "$" - }} - {{ "freeWithSponsorship" | i18n }} - - - {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} - - -

-

- - {{ "additionalUsers" | i18n }}: - {{ sub?.seats || 0 }}  - {{ "members" | i18n }} - × - {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - - {{ "freeForOneYear" | i18n }} - - - - {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} - -

-
- - -

- {{ "secretsManager" | i18n }} -

-

- - {{ "basePrice" | i18n }}: - {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} - {{ "monthAbbr" | i18n }} - - - {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} - -

-

- - {{ "additionalUsers" | i18n }}: - {{ sub?.smSeats }}  - {{ "members" | i18n }} - × - {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - {{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} - -

-

- - {{ additionalServiceAccount }} - {{ "serviceAccounts" | i18n }} - × - {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} -

- -

- {{ "passwordManager" | i18n }} -

-

- - {{ "basePrice" | i18n }}: - {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} - {{ "monthAbbr" | i18n }} - - - {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} - -

-

- - {{ "additionalUsers" | i18n }}: - {{ sub?.seats }}  - {{ "members" | i18n }} - × - {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ selectedPlanInterval | i18n }} - - - {{ "freeForOneYear" | i18n }} - - - - {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} - -

-
-
- -
- -

- - - {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} - - {{ - calculateTotalAppliedDiscount(total) | currency: "$" - }} - -

-
-
-
- -

- - {{ "estimatedTax" | i18n }} - - - {{ estimatedTax | currency: "USD" : "$" }} - -

-
-
-
- -

- - {{ "total" | i18n }} - - - {{ total - calculateTotalAppliedDiscount(total) | currency: "USD" : "$" }} - - / {{ selectedPlanInterval | i18n }} - -

-
-
+ + diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index dc8474d24d6..8af7aba1d29 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -65,8 +65,10 @@ import { import { KeyService } from "@bitwarden/key-management"; import { BillingNotificationService } from "../services/billing-notification.service"; +import { PricingSummaryService } from "../services/pricing-summary.service"; import { BillingSharedModule } from "../shared/billing-shared.module"; import { PaymentComponent } from "../shared/payment/payment.component"; +import { PricingSummaryData } from "../shared/pricing-summary/pricing-summary.component"; type ChangePlanDialogParams = { organizationId: string; @@ -130,7 +132,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup?.controls?.productTier?.setValue(product); } - protected estimatedTax: number = 0; private _productTier = ProductTierType.Free; @Input() @@ -186,7 +187,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { dialogHeaderName: string; currentPlanName: string; showPayment: boolean = false; - totalOpened: boolean = false; currentPlan: PlanResponse; isCardStateDisabled = false; focusedIndex: number | null = null; @@ -195,6 +195,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { plans: ListResponse; isSubscriptionCanceled: boolean = false; secretsManagerTotal: number; + pricingSummaryData: PricingSummaryData; private destroy$ = new Subject(); @@ -219,6 +220,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private accountService: AccountService, private organizationBillingService: OrganizationBillingService, private billingNotificationService: BillingNotificationService, + private pricingSummaryService: PricingSummaryService, ) {} async ngOnInit(): Promise { @@ -312,6 +314,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); this.taxInformation = TaxInformation.from(taxInfo); + // Initialize pricing summary data + await this.updatePricingSummaryData(); + if (!this.isSubscriptionCanceled) { this.refreshSalesTax(); } @@ -335,7 +340,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { setInitialPlanSelection() { this.focusedIndex = this.selectableProducts.length - 1; if (!this.isSubscriptionCanceled) { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + const enterprisePlan = this.getPlanByType(ProductTierType.Enterprise); + const planToSelect = + enterprisePlan || this.selectableProducts[this.selectableProducts.length - 1]; + if (planToSelect) { + this.selectPlan(planToSelect); + } } } @@ -362,6 +372,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { updateInterval(event: number) { this.selectedInterval = event; this.planTypeChanged(); + void this.updatePricingSummaryData(); } protected getPlanIntervals() { @@ -460,6 +471,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } protected selectPlan(plan: PlanResponse) { + if (!plan) { + return; + } + if ( this.selectedInterval === PlanInterval.Monthly && plan.productTier == ProductTierType.Families @@ -476,7 +491,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { try { this.refreshSalesTax(); } catch { - this.estimatedTax = 0; + void this.updatePricingSummaryData(); } } @@ -585,83 +600,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0; } - passwordManagerSeatTotal(plan: PlanResponse): number { - if (!plan.PasswordManager.hasAdditionalSeatsOption || this.isSecretsManagerTrial()) { - return 0; - } - - const result = plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0); - return result; - } - - secretsManagerSeatTotal(plan: PlanResponse, seats: number): number { - if (!plan.SecretsManager.hasAdditionalSeatsOption) { - return 0; - } - - return plan.SecretsManager.seatPrice * Math.abs(seats || 0); - } - - additionalStorageTotal(plan: PlanResponse): number { - if (!plan.PasswordManager.hasAdditionalStorageOption) { - return 0; - } - - return ( - plan.PasswordManager.additionalStoragePricePerGb * - // TODO: Eslint upgrade. Please resolve this since the null check does nothing - // eslint-disable-next-line no-constant-binary-expression - Math.abs(this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0 || 0) - ); - } - - additionalStoragePriceMonthly(selectedPlan: PlanResponse) { - return selectedPlan.PasswordManager.additionalStoragePricePerGb; - } - - additionalServiceAccountTotal(plan: PlanResponse): number { - if ( - !plan.SecretsManager.hasAdditionalServiceAccountOption || - this.additionalServiceAccount == 0 - ) { - return 0; - } - - return plan.SecretsManager.additionalPricePerServiceAccount * this.additionalServiceAccount; - } - - get passwordManagerSubtotal() { - if (!this.selectedPlan || !this.selectedPlan.PasswordManager) { - return 0; - } - - let subTotal = this.selectedPlan.PasswordManager.basePrice; - if (this.selectedPlan.PasswordManager.hasAdditionalSeatsOption) { - subTotal += this.passwordManagerSeatTotal(this.selectedPlan); - } - if (this.selectedPlan.PasswordManager.hasPremiumAccessOption) { - subTotal += this.selectedPlan.PasswordManager.premiumAccessOptionPrice; - } - return subTotal - this.discount; - } - - secretsManagerSubtotal() { - const plan = this.selectedPlan; - if (!plan || !plan.SecretsManager) { - return this.secretsManagerTotal || 0; - } - - if (this.secretsManagerTotal) { - return this.secretsManagerTotal; - } - - this.secretsManagerTotal = - plan.SecretsManager.basePrice + - this.secretsManagerSeatTotal(plan, this.sub?.smSeats) + - this.additionalServiceAccountTotal(plan); - return this.secretsManagerTotal; - } - get passwordManagerSeats() { if (!this.selectedPlan) { return 0; @@ -673,26 +611,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.sub?.seats; } - get total() { - if (!this.organization || !this.selectedPlan) { - return 0; - } - - if (this.organization.useSecretsManager) { - return ( - this.passwordManagerSubtotal + - this.additionalStorageTotal(this.selectedPlan) + - this.secretsManagerSubtotal() + - this.estimatedTax - ); - } - return ( - this.passwordManagerSubtotal + - this.additionalStorageTotal(this.selectedPlan) + - this.estimatedTax - ); - } - get teamsStarterPlanIsAvailable() { return this.selectablePlans.some((plan) => plan.type === PlanType.TeamsStarter); } @@ -984,15 +902,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.showPayment = true; } - toggleTotalOpened() { - this.totalOpened = !this.totalOpened; - } - - calculateTotalAppliedDiscount(total: number) { - const discountedTotal = total * (this.discountPercentageFromSub / 100); - return discountedTotal; - } - get paymentSourceClasses() { if (this.paymentSource == null) { return []; @@ -1099,7 +1008,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.taxService .previewOrganizationInvoice(request) .then((invoice) => { - this.estimatedTax = invoice.taxAmount; + void this.updatePricingSummaryData(); }) .catch((error) => { const translatedMessage = this.i18nService.t(error.message); @@ -1112,6 +1021,21 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { }); } + private async updatePricingSummaryData(): Promise { + if (!this.selectedPlan || !this.organization || !this.taxInformation) { + return; + } + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.selectedPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + } + protected canUpdatePaymentInformation(): boolean { return ( this.upgradeRequiresPaymentMethod || diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 7322f047551..5e2d9f656b9 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -60,6 +60,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, + PricingSummaryComponent, ], }) export class BillingSharedModule {}