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 14b6864d64..766646003b 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 @@ -7,31 +7,37 @@

{{ "upgradePlans" | i18n }}

{{ "selectAPlan" | i18n }} +
{{ "upgradeDiscount" | i18n - : (this.discountPercentageFromSub > 0 - ? discountPercentageFromSub - : this.discountPercentage) + : (selectedInterval === planIntervals.Annually + ? discountPercentageFromSub + this.discountPercentage + : this.discountPercentageFromSub) }} - +
{{ planInterval.name }} @@ -40,6 +46,7 @@
+
{{ "recommended" | i18n }}
-

+

{{ selectableProduct.nameLocalizationKey | i18n }} - + {{ "current" | i18n }}

@@ -133,10 +151,13 @@ else nonEnterprisePlans " > -

+

{{ "bitwardenPasswordManager" | i18n }}

-

{{ "enterprisePlanUpgradeMessage" | i18n }}

+

{{ "enterprisePlanUpgradeMessage" | i18n }}

  • @@ -157,7 +178,10 @@
-

+

{{ "bitwardenSecretsManager" | i18n }}

    @@ -195,25 +219,25 @@

    {{ "bitwardenPasswordManager" | i18n }}

    {{ "teamsPlanUpgradeMessage" | i18n }}

    {{ "familyPlanUpgradeMessage" | i18n }}

    • @@ -247,7 +271,7 @@

    {{ "secretsManagerSubInfo" | i18n }} - {{ "secretsManagerWithFreePasswordManagerInfo" | i18n }} + {{ "secretsManagerComplimentaryPasswordManager" | i18n }}
    @@ -392,23 +416,37 @@

    - {{ organization.maxStorageGb }} + {{ storageGb }} {{ "additionalStorageGbMessage" | i18n }} × {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} /{{ "year" | i18n }} - {{ - organization.maxStorageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb - | currency: "$" - }} + {{ additionalStorageTotal(selectedPlan) | currency: "$" }} +

    + +

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

    @@ -459,18 +497,40 @@ bitTypography="body2" *ngIf=" selectedPlan?.SecretsManager?.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n | lowercase }} × {{ selectedPlan?.SecretsManager?.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

    + +

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

    @@ -512,24 +572,39 @@

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

    + +

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

    {{ "secretsManager" | i18n }} @@ -575,18 +650,41 @@ bitTypography="body2" *ngIf=" selectedPlan.SecretsManager.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n | lowercase }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

    + +

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

@@ -641,18 +739,40 @@ bitTypography="body2" *ngIf=" selectedPlan.SecretsManager.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

+ +

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

{{ "passwordManager" | i18n }} @@ -663,7 +783,7 @@ *ngIf="selectedPlan.PasswordManager.basePrice" > - {{ organization.seats }} + {{ sub?.seats }} {{ "members" | i18n }} × {{ (selectedPlan.isAnnual @@ -694,7 +814,7 @@ {{ "additionalUsers" | i18n }}: - {{ organization.seats || 0 }}  + {{ sub?.seats || 0 }}  {{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} @@ -756,12 +876,12 @@ bitTypography="body2" *ngIf=" selectedPlan.SecretsManager.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} @@ -795,7 +915,7 @@ {{ "additionalUsers" | i18n }}: - {{ organization.seats }}  + {{ sub?.seats }}  {{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} @@ -811,6 +931,46 @@

+ +
+ +

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

+
+

{{ total | currency: "USD" : "$" }} - / {{ selectedPlanInterval | i18n }} + + / {{ 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 9a20fe38ef..dc9f6cce68 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 @@ -246,27 +246,28 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { selected: false, }, ]; - this.discountPercentageFromSub = this.sub?.customerDiscount?.percentOff; + this.discountPercentageFromSub = this.isSecretsManagerTrial() + ? 0 + : (this.sub?.customerDiscount?.percentOff ?? 0); this.setInitialPlanSelection(); this.loading = false; } setInitialPlanSelection() { - if ( - this.organization.useSecretsManager && - this.currentPlan.productTier == ProductTierType.Free - ) { - this.selectPlan(this.getPlanByType(ProductTierType.Teams)); - } else { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); - } + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } getPlanByType(productTier: ProductTierType) { return this.selectableProducts.find((product) => product.productTier === productTier); } + secretsManagerTrialDiscount() { + return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") + ? this.discountPercentage + : this.discountPercentageFromSub + this.discountPercentage; + } + isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -276,14 +277,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } planTypeChanged() { - if ( - this.organization.useSecretsManager && - this.currentPlan.productTier == ProductTierType.Free - ) { - this.selectPlan(this.getPlanByType(ProductTierType.Teams)); - } else { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); - } + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } updateInterval(event: number) { @@ -304,6 +298,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ]; } + optimizedNgForRender(index: number) { + return index; + } + protected getPlanCardContainerClasses(plan: PlanResponse, index: number) { let cardState: PlanCardState; @@ -370,6 +368,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ) { return; } + + if (plan === this.currentPlan) { + return; + } this.selectedPlan = plan; this.formGroup.patchValue({ productTier: plan.productTier }); } @@ -463,6 +465,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return result; } + get storageGb() { + return this.sub?.maxStorageGb - 1; + } + passwordManagerSeatTotal(plan: PlanResponse): number { if (!plan.PasswordManager.hasAdditionalSeatsOption || this.isSecretsManagerTrial()) { return 0; @@ -486,8 +492,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } return ( - plan.PasswordManager.additionalStoragePricePerGb * - Math.abs(this.organization.maxStorageGb || 0) + plan.PasswordManager.additionalStoragePricePerGb * Math.abs(this.sub?.maxStorageGb - 1 || 0) ); } @@ -499,7 +504,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } additionalServiceAccountTotal(plan: PlanResponse): number { - if (!plan.SecretsManager.hasAdditionalServiceAccountOption || this.additionalServiceAccount) { + if ( + !plan.SecretsManager.hasAdditionalServiceAccountOption || + this.additionalServiceAccount == 0 + ) { return 0; } @@ -541,7 +549,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { if (this.selectedPlan.productTier === ProductTierType.Families) { return this.selectedPlan.PasswordManager.baseSeats; } - return this.organization.seats; + return this.sub?.seats; } get total() { @@ -565,7 +573,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get additionalServiceAccount() { - const baseServiceAccount = this.selectedPlan.SecretsManager?.baseServiceAccount || 0; + const baseServiceAccount = this.currentPlan.SecretsManager?.baseServiceAccount || 0; const usedServiceAccounts = this.sub?.smServiceAccounts || 0; const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts; @@ -652,7 +660,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { if (!this.acceptingSponsorship && !this.isInTrialFlow) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/organizations/" + orgId]); + this.router.navigate(["/organizations/" + orgId + "/members"]); } if (this.isInTrialFlow) { @@ -676,11 +684,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private async updateOrganization() { const request = new OrganizationUpgradeRequest(); if (this.selectedPlan.productTier !== ProductTierType.Families) { - request.additionalSeats = this.organization.seats; + request.additionalSeats = this.sub?.seats; } - if (this.organization.maxStorageGb > this.selectedPlan.PasswordManager.baseStorageGb) { + if (this.sub?.maxStorageGb > this.selectedPlan.PasswordManager.baseStorageGb) { request.additionalStorageGb = - this.organization.maxStorageGb - this.selectedPlan.PasswordManager.baseStorageGb; + this.sub?.maxStorageGb - this.selectedPlan.PasswordManager.baseStorageGb; } request.premiumAccessAddon = this.selectedPlan.PasswordManager.hasPremiumAccessOption && @@ -768,6 +776,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { request.additionalSmSeats = this.organization.seats; } else { request.additionalSmSeats = this.sub?.smSeats; + request.additionalServiceAccounts = this.additionalServiceAccount; } } @@ -812,6 +821,16 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.totalOpened = !this.totalOpened; } + calculateTotalAppliedDiscount(total: number) { + const discountPercent = + this.selectedInterval == PlanInterval.Annually + ? this.discountPercentage + this.discountPercentageFromSub + : this.discountPercentageFromSub; + + const discountedTotal = total / (1 - discountPercent / 100); + return discountedTotal; + } + get paymentSourceClasses() { if (this.billing.paymentSource == null) { return []; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 25c8c547b2..341324c4a2 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -69,14 +69,25 @@ >
- {{ - "details" | i18n - }} + {{ "details" | i18n + }}{{ "providerDiscount" | i18n: customerDiscount?.percentOff }} - + {{ i.productName | i18n }} - {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ {{ i.amount | currency: "$" }} @@ -91,7 +102,19 @@ {{ "freeForOneYear" | i18n }} - {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} +
+ + {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} + + {{ + calculateTotalAppliedDiscount(i.quantity * i.amount) | currency: "$" + }} + / {{ "year" | i18n }} +
@@ -112,7 +135,7 @@ -
+