diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts
index adbda79caf2..71941779508 100644
--- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts
+++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts
@@ -34,8 +34,8 @@ import {
AdjustPaymentDialogResultType,
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
import {
+ TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE,
TrialPaymentMethodDialogComponent,
- TrialPaymentMethodDialogResultType,
} from "../../shared/trial-subscription-dialog/trial-payment-method-dialog.component";
import { FreeTrial } from "../../types/free-trial";
@@ -202,7 +202,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
},
});
const result = await lastValueFrom(dialogRef.closed);
- if (result === TrialPaymentMethodDialogResultType.Submitted) {
+ if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) {
this.location.replaceState(this.location.path(), "", {});
if (this.launchPaymentModalAutomatically && !this.organization.enabled) {
await this.syncService.fullSync(true);
diff --git a/apps/web/src/app/billing/shared/trial-subscription-dialog/plan-selection.service.ts b/apps/web/src/app/billing/shared/trial-subscription-dialog/plan-selection.service.ts
new file mode 100644
index 00000000000..745a52e7199
--- /dev/null
+++ b/apps/web/src/app/billing/shared/trial-subscription-dialog/plan-selection.service.ts
@@ -0,0 +1,113 @@
+import { Injectable } from "@angular/core";
+
+import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums";
+import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
+
+@Injectable()
+export class PlanSelectionService {
+ getPlanCardContainerClasses(
+ plan: PlanResponse,
+ index: number,
+ isCardDisabled: (index: number) => boolean,
+ ): string[] {
+ const isSelected = plan.isAnnual;
+ const isDisabled = isCardDisabled(index);
+
+ if (isDisabled) {
+ return [
+ "tw-cursor-not-allowed",
+ "tw-bg-secondary-100",
+ "tw-font-normal",
+ "tw-bg-blur",
+ "tw-text-muted",
+ "tw-block",
+ "tw-rounded",
+ ];
+ }
+
+ return isSelected
+ ? [
+ "tw-cursor-pointer",
+ "tw-block",
+ "tw-rounded",
+ "tw-border",
+ "tw-border-solid",
+ "tw-border-primary-600",
+ "hover:tw-border-primary-700",
+ "focus:tw-border-2",
+ "focus:tw-border-primary-700",
+ "focus:tw-rounded-lg",
+ ]
+ : [
+ "tw-cursor-pointer",
+ "tw-block",
+ "tw-rounded",
+ "tw-border",
+ "tw-border-solid",
+ "tw-border-secondary-300",
+ "hover:tw-border-text-main",
+ "focus:tw-border-2",
+ "focus:tw-border-primary-700",
+ ];
+ }
+
+ selectPlan(
+ plan: PlanResponse,
+ selectedInterval: number,
+ currentPlan: PlanResponse,
+ onPlanSelected: (selectedPlan: PlanResponse) => void,
+ ): void {
+ if (
+ selectedInterval === PlanInterval.Monthly &&
+ plan.productTier === ProductTierType.Families
+ ) {
+ return;
+ }
+
+ onPlanSelected(plan);
+ }
+
+ getSelectablePlans(
+ passwordManagerPlans: PlanResponse[],
+ selectedPlan: PlanResponse,
+ planIsEnabled: (plan: PlanResponse) => boolean,
+ ): PlanResponse[] {
+ const result =
+ passwordManagerPlans?.filter(
+ (plan) => plan.productTier === selectedPlan.productTier && planIsEnabled(plan),
+ ) || [];
+
+ result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder).reverse();
+ return result;
+ }
+
+ handleKeydown(
+ event: KeyboardEvent,
+ index: number,
+ isCardDisabled: (index: number) => boolean,
+ ): void {
+ const cardElements = Array.from(document.querySelectorAll(".product-card")) as HTMLElement[];
+ let newIndex = index;
+ const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1;
+
+ if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
+ do {
+ newIndex = (newIndex + direction + cardElements.length) % cardElements.length;
+ } while (isCardDisabled(newIndex) && newIndex !== index);
+
+ event.preventDefault();
+
+ setTimeout(() => {
+ const card = cardElements[newIndex];
+ if (
+ !(
+ card.classList.contains("tw-bg-secondary-100") &&
+ card.classList.contains("tw-text-muted")
+ )
+ ) {
+ card?.focus();
+ }
+ }, 0);
+ }
+ }
+}
diff --git a/apps/web/src/app/billing/shared/trial-subscription-dialog/pricing-calculation.service.ts b/apps/web/src/app/billing/shared/trial-subscription-dialog/pricing-calculation.service.ts
new file mode 100644
index 00000000000..b60773d3f2e
--- /dev/null
+++ b/apps/web/src/app/billing/shared/trial-subscription-dialog/pricing-calculation.service.ts
@@ -0,0 +1,153 @@
+import { Injectable } from "@angular/core";
+
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
+import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
+import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
+
+@Injectable()
+export class PricingCalculationService {
+ calculatePasswordManagerSubtotal(
+ selectedPlan: PlanResponse,
+ subscription: OrganizationSubscriptionResponse,
+ discount: number,
+ ): number {
+ if (!selectedPlan || !selectedPlan.PasswordManager) {
+ return 0;
+ }
+
+ let subTotal = selectedPlan.PasswordManager.basePrice;
+ if (selectedPlan.PasswordManager.hasAdditionalSeatsOption) {
+ subTotal += this.calculatePasswordManagerSeatTotal(selectedPlan, subscription, false);
+ }
+ if (selectedPlan.PasswordManager.hasPremiumAccessOption) {
+ subTotal += selectedPlan.PasswordManager.premiumAccessOptionPrice;
+ }
+ return subTotal - discount;
+ }
+
+ calculateSecretsManagerSubtotal(
+ selectedPlan: PlanResponse,
+ subscription: OrganizationSubscriptionResponse,
+ secretsManagerTotal: number,
+ ): number {
+ const plan = selectedPlan;
+ if (!plan || !plan.SecretsManager) {
+ return secretsManagerTotal || 0;
+ }
+
+ if (secretsManagerTotal) {
+ return secretsManagerTotal;
+ }
+
+ return (
+ plan.SecretsManager.basePrice +
+ this.calculateSecretsManagerSeatTotal(plan, subscription?.smSeats) +
+ this.calculateAdditionalServiceAccountTotal(plan, 0)
+ ); // This will be calculated separately
+ }
+
+ getPasswordManagerSeats(
+ selectedPlan: PlanResponse,
+ subscription: OrganizationSubscriptionResponse,
+ ): number {
+ if (!selectedPlan) {
+ return 0;
+ }
+
+ if (selectedPlan.productTier === ProductTierType.Families) {
+ return selectedPlan.PasswordManager.baseSeats;
+ }
+ return subscription?.seats;
+ }
+
+ calculateTotal(
+ organization: Organization,
+ selectedPlan: PlanResponse,
+ passwordManagerSubtotal: number,
+ estimatedTax: number,
+ subscription: OrganizationSubscriptionResponse,
+ ): number {
+ if (!organization || !selectedPlan) {
+ return 0;
+ }
+
+ if (organization.useSecretsManager) {
+ return (
+ this.calculateAdditionalStorageTotal(selectedPlan, subscription) +
+ this.calculateSecretsManagerSubtotal(selectedPlan, subscription, 0) +
+ estimatedTax
+ );
+ }
+ return (
+ passwordManagerSubtotal +
+ this.calculateAdditionalStorageTotal(selectedPlan, subscription) +
+ estimatedTax
+ );
+ }
+
+ calculateAdditionalServiceAccount(
+ currentPlan: PlanResponse,
+ subscription: OrganizationSubscriptionResponse,
+ ): number {
+ if (!currentPlan || !currentPlan.SecretsManager) {
+ return 0;
+ }
+
+ const baseServiceAccount = currentPlan.SecretsManager?.baseServiceAccount || 0;
+ const usedServiceAccounts = subscription?.smServiceAccounts || 0;
+
+ const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts;
+
+ return additionalServiceAccounts <= 0 ? Math.abs(additionalServiceAccounts) : 0;
+ }
+
+ calculatePasswordManagerSeatTotal(
+ plan: PlanResponse,
+ subscription: OrganizationSubscriptionResponse,
+ isSecretsManagerTrial: boolean,
+ ): number {
+ if (!plan.PasswordManager.hasAdditionalSeatsOption || isSecretsManagerTrial) {
+ return 0;
+ }
+
+ return plan.PasswordManager.seatPrice * Math.abs(subscription?.seats || 0);
+ }
+
+ calculateSecretsManagerSeatTotal(plan: PlanResponse, seats: number): number {
+ if (!plan.SecretsManager.hasAdditionalSeatsOption) {
+ return 0;
+ }
+
+ return plan.SecretsManager.seatPrice * Math.abs(seats || 0);
+ }
+
+ calculateAdditionalStorageTotal(
+ plan: PlanResponse,
+ subscription: OrganizationSubscriptionResponse,
+ ): number {
+ if (!plan.PasswordManager.hasAdditionalStorageOption) {
+ return 0;
+ }
+
+ return (
+ plan.PasswordManager.additionalStoragePricePerGb *
+ Math.abs(subscription?.maxStorageGb ? subscription.maxStorageGb - 1 : 0)
+ );
+ }
+
+ calculateAdditionalServiceAccountTotal(
+ plan: PlanResponse,
+ additionalServiceAccount: number,
+ ): number {
+ if (!plan.SecretsManager.hasAdditionalServiceAccountOption || additionalServiceAccount === 0) {
+ return 0;
+ }
+
+ return plan.SecretsManager.additionalPricePerServiceAccount * additionalServiceAccount;
+ }
+
+ calculateTotalAppliedDiscount(total: number, discountPercentageFromSub: number): number {
+ return total * (discountPercentageFromSub / 100);
+ }
+}
diff --git a/apps/web/src/app/billing/shared/trial-subscription-dialog/trial-payment-method-dialog.component.html b/apps/web/src/app/billing/shared/trial-subscription-dialog/trial-payment-method-dialog.component.html
index cd27a443570..f1d917ff353 100644
--- a/apps/web/src/app/billing/shared/trial-subscription-dialog/trial-payment-method-dialog.component.html
+++ b/apps/web/src/app/billing/shared/trial-subscription-dialog/trial-payment-method-dialog.component.html
@@ -3,66 +3,69 @@
{{ "subscribetoEnterprise" | i18n: currentPlanName }}
+
{{ "subscribetoEnterpriseSubtitle" | i18n: currentPlanName }}
-
- -
-
- {{ "includeEnterprisePolicies" | i18n }}
-
- -
-
- {{ "passwordLessSso" | i18n }}
-
- -
-
- {{ "accountRecovery" | i18n }}
-
- -
-
- {{ "customRoles" | i18n }}
-
- -
-
- {{ "unlimitedSecretsAndProjects" | i18n }}
-
-
+
+
+ -
+
+ {{ "includeEnterprisePolicies" | i18n }}
+
+ -
+
+ {{ "passwordLessSso" | i18n }}
+
+ -
+
+ {{ "accountRecovery" | i18n }}
+
+ -
+
+ {{ "customRoles" | i18n }}
+
+ -
+
+ {{ "unlimitedSecretsAndProjects" | i18n }}
+
+
-
- -
-
- {{ "secureDataSharing" | i18n }}
-
- -
-
- {{ "eventLogMonitoring" | i18n }}
-
- -
-
- {{ "directoryIntegration" | i18n }}
-
- -
-
- {{ "unlimitedSecretsAndProjects" | i18n }}
-
-
+
+ -
+
+ {{ "secureDataSharing" | i18n }}
+
+ -
+
+ {{ "eventLogMonitoring" | i18n }}
+
+ -
+
+ {{ "directoryIntegration" | i18n }}
+
+ -
+
+ {{ "unlimitedSecretsAndProjects" | i18n }}
+
+
-
- -
-
- {{ "premiumAccounts" | i18n }}
-
- -
-
- {{ "unlimitedSharing" | i18n }}
-
- -
-
- {{ "createUnlimitedCollections" | i18n }}
-
-
+
+ -
+
+ {{ "premiumAccounts" | i18n }}
+
+ -
+
+ {{ "unlimitedSharing" | i18n }}
+
+ -
+
+ {{ "createUnlimitedCollections" | i18n }}
+
+
+
@@ -219,607 +222,88 @@
>
-
-
-
-
- {{ "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: "$" }}
-
-
- 0"
- >
-
- {{ storageGb }}
- {{ "additionalStorageGbMessage" | i18n }}
- ×
- {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }}
- /{{ selectedPlanInterval | i18n }}
-
- {{ additionalStorageTotal(selectedPlan) | currency: "$" }}
-
-
-
- 0"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0"
>
-
- {{ "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: "$" }}
-
-
- 0
- "
- >
-
- {{ additionalServiceAccount }}
- {{ "serviceAccounts" | i18n | lowercase }}
- ×
- {{ selectedPlan?.SecretsManager?.additionalPricePerServiceAccount | currency: "$" }}
- /{{ selectedPlanInterval | i18n }}
-
- {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}
-
-
-
- 0"
- >
-
- {{ "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: "$" }}
-
-
- 0"
- >
-
- {{ storageGb }}
- {{ "additionalStorageGbMessage" | i18n }}
- ×
- {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }}
- /{{ selectedPlanInterval | i18n }}
-
- {{
- storageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$"
- }}
-
-
-
-
- 0 ? 'block' : 'none'"
- >
- {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }}
-
- 0 ? 'block' : 'none'"
- class="tw-line-through tw-text-xs"
- >{{ 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: "$" }}
-
-
- 0
- "
- >
-
- {{ additionalServiceAccount }}
- {{ "serviceAccounts" | i18n | lowercase }}
- ×
- {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }}
- /{{ selectedPlanInterval | i18n }}
-
- {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}
-
-
-
-
- 0 ? 'block' : 'none'"
- >
- {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }}
-
- 0 ? 'block' : 'none'"
- class="tw-line-through tw-text-xs"
- >{{
- 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: "$" }}
-
-
- 0
- "
- >
-
- {{ 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: "$" }}
-
-
- 0
- "
- >
-
- {{ 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: "$" }}
-
-
-
-
-
-
-
- 0"
- >
-
{{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }}
{{
calculateTotalAppliedDiscount(total) | currency: "$"
}}
-
-
-
-
-
-
-
-
-
- {{ "estimatedTax" | i18n }}
-
-
- {{ estimatedTax | currency: "USD" : "$" }}
-
-
-
-
+
+
+
-
-
-
-
- {{ "total" | i18n }}
-
-
- {{ total - calculateTotalAppliedDiscount(total) | currency: "USD" : "$" }}
-
- / {{ selectedPlanInterval | i18n }}
-
-
-
-
+
+
+
+
+
+ {{ "estimatedTax" | i18n }}
+
+
+ {{ estimatedTax | currency: "USD" : "$" }}
+
+
+
+
+
+
+
+
+
+ {{ "total" | i18n }}
+
+
+ {{ total - calculateTotalAppliedDiscount(total) | currency: "USD" : "$" }}
+
+ / {{ selectedPlanInterval | i18n }}
+
+
+
+
+
@@ -828,9 +312,316 @@
-