diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 21149499485..6a7df1678bf 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -1475,6 +1475,15 @@
"ppremiumSignUpStorage": {
"message": "1 GB encrypted storage for file attachments."
},
+ "premiumSignUpStorageV2": {
+ "message": "$SIZE$ encrypted storage for file attachments.",
+ "placeholders": {
+ "size": {
+ "content": "$1",
+ "example": "1 GB"
+ }
+ }
+ },
"premiumSignUpEmergency": {
"message": "Emergency access."
},
diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.html b/apps/browser/src/billing/popup/settings/premium-v2.component.html
index 47d72751af3..fea3e558057 100644
--- a/apps/browser/src/billing/popup/settings/premium-v2.component.html
+++ b/apps/browser/src/billing/popup/settings/premium-v2.component.html
@@ -12,7 +12,7 @@
-
- {{ "ppremiumSignUpStorage" | i18n }}
+ {{ "premiumSignUpStorageV2" | i18n: `${storageProvidedGb} GB` }}
-
{{ "premiumSignUpTwoStepOptions" | i18n }}
diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts
index b858b74242d..0c246d734e5 100644
--- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts
+++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts
@@ -1,13 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule, CurrencyPipe, Location } from "@angular/common";
-import { Component } from "@angular/core";
+import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/billing/components/premium.component";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -44,7 +45,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
SectionComponent,
],
})
-export class PremiumV2Component extends BasePremiumComponent {
+export class PremiumV2Component extends BasePremiumComponent implements OnInit {
priceString: string;
constructor(
@@ -59,6 +60,7 @@ export class PremiumV2Component extends BasePremiumComponent {
billingAccountProfileStateService: BillingAccountProfileStateService,
toastService: ToastService,
accountService: AccountService,
+ billingApiService: BillingApiServiceAbstraction,
) {
super(
i18nService,
@@ -70,15 +72,18 @@ export class PremiumV2Component extends BasePremiumComponent {
billingAccountProfileStateService,
toastService,
accountService,
+ billingApiService,
);
-
+ }
+ async ngOnInit() {
+ await super.ngOnInit();
// Support old price string. Can be removed in future once all translations are properly updated.
const thePrice = this.currencyPipe.transform(this.price, "$");
// Safari extension crashes due to $1 appearing in the price string ($10.00). Escape the $ to fix.
const formattedPrice = this.platformUtilsService.isSafari()
? thePrice.replace("$", "$$$")
: thePrice;
- this.priceString = i18nService.t("premiumPriceV2", formattedPrice);
+ this.priceString = this.i18nService.t("premiumPriceV2", formattedPrice);
if (this.priceString.indexOf("%price%") > -1) {
this.priceString = this.priceString.replace("%price%", thePrice);
}
diff --git a/apps/desktop/src/billing/app/accounts/premium.component.html b/apps/desktop/src/billing/app/accounts/premium.component.html
index d88602bed1e..c5f9722f133 100644
--- a/apps/desktop/src/billing/app/accounts/premium.component.html
+++ b/apps/desktop/src/billing/app/accounts/premium.component.html
@@ -13,7 +13,7 @@
-
- {{ "premiumSignUpStorage" | i18n }}
+ {{ "premiumSignUpStorageV2" | i18n: `${storageProvidedGb} GB` }}
-
diff --git a/apps/desktop/src/billing/app/accounts/premium.component.ts b/apps/desktop/src/billing/app/accounts/premium.component.ts
index 637969c1a21..4aff0cc03e1 100644
--- a/apps/desktop/src/billing/app/accounts/premium.component.ts
+++ b/apps/desktop/src/billing/app/accounts/premium.component.ts
@@ -3,6 +3,7 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/billing/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -28,6 +29,7 @@ export class PremiumComponent extends BasePremiumComponent {
billingAccountProfileStateService: BillingAccountProfileStateService,
toastService: ToastService,
accountService: AccountService,
+ billingApiService: BillingApiServiceAbstraction,
) {
super(
i18nService,
@@ -39,6 +41,7 @@ export class PremiumComponent extends BasePremiumComponent {
billingAccountProfileStateService,
toastService,
accountService,
+ billingApiService,
);
}
}
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index f6f078611c9..757059c4e41 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -1490,6 +1490,15 @@
"premiumSignUpStorage": {
"message": "1 GB encrypted storage for file attachments."
},
+ "premiumSignUpStorageV2": {
+ "message": "$SIZE$ encrypted storage for file attachments.",
+ "placeholders": {
+ "size": {
+ "content": "$1",
+ "example": "1 GB"
+ }
+ }
+ },
"premiumSignUpTwoStepOptions": {
"message": "Proprietary two-step login options such as YubiKey and Duo."
},
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html
index 63c26bd61f1..33e89f21fc0 100644
--- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html
+++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html
@@ -24,7 +24,7 @@
-
- {{ "premiumSignUpStorage" | i18n }}
+ {{ "premiumSignUpStorageV2" | i18n: `${(providedStorageGb$ | async)} GB` }}
-
@@ -82,7 +82,10 @@
/>
{{
"additionalStorageIntervalDesc"
- | i18n: "1 GB" : (storagePrice$ | async | currency: "$") : ("year" | i18n)
+ | i18n
+ : `${(providedStorageGb$ | async)} GB`
+ : (storagePrice$ | async | currency: "$")
+ : ("year" | i18n)
}}
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts
index fceeeedf170..86a508d2701 100644
--- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts
+++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts
@@ -22,8 +22,8 @@ import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
+import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
-import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service";
import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -75,6 +75,7 @@ export class CloudHostedPremiumComponent {
return {
seat: premiumPlan.passwordManager.annualPrice,
storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB,
+ providedStorageGb: premiumPlan.passwordManager.providedStorageGB,
};
}),
shareReplay({ bufferSize: 1, refCount: true }),
@@ -84,6 +85,8 @@ export class CloudHostedPremiumComponent {
storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage));
+ providedStorageGb$ = this.premiumPrices$.pipe(map((prices) => prices.providedStorageGb));
+
protected isLoadingPrices$ = this.premiumPrices$.pipe(
map(() => false),
startWith(true),
@@ -134,7 +137,7 @@ export class CloudHostedPremiumComponent {
private accountService: AccountService,
private subscriberBillingClient: SubscriberBillingClient,
private taxClient: TaxClient,
- private subscriptionPricingService: DefaultSubscriptionPricingService,
+ private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
) {
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
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 0fd7746fc9d..978bb35c5c7 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
@@ -620,7 +620,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
get storageGb() {
- return this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0;
+ return Math.max(
+ 0,
+ (this.sub?.maxStorageGb ?? 0) - this.selectedPlan.PasswordManager.baseStorageGb,
+ );
}
passwordManagerSeatTotal(plan: PlanResponse): number {
@@ -644,12 +647,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
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)
- );
+ return plan.PasswordManager.additionalStoragePricePerGb * this.storageGb;
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html
index 6234fc6e6e3..d06604ba29e 100644
--- a/apps/web/src/app/billing/organizations/organization-plans.component.html
+++ b/apps/web/src/app/billing/organizations/organization-plans.component.html
@@ -104,7 +104,7 @@
{{
"gbEncryptedFileStorage"
- | i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
+ | i18n: selectableProduct.PasswordManager.baseStorageGb + " GB"
}}
@@ -239,7 +239,7 @@
{{
"additionalStorageIntervalDesc"
| i18n
- : "1 GB"
+ : `${selectedPlan.PasswordManager.baseStorageGb} GB`
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
: ("month" | i18n)
}}
diff --git a/apps/web/src/app/billing/services/pricing-summary.service.spec.ts b/apps/web/src/app/billing/services/pricing-summary.service.spec.ts
new file mode 100644
index 00000000000..4e15d318a03
--- /dev/null
+++ b/apps/web/src/app/billing/services/pricing-summary.service.spec.ts
@@ -0,0 +1,232 @@
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums";
+import {
+ BillingCustomerDiscount,
+ OrganizationSubscriptionResponse,
+} from "@bitwarden/common/billing/models/response/organization-subscription.response";
+import {
+ PasswordManagerPlanFeaturesResponse,
+ PlanResponse,
+ SecretsManagerPlanFeaturesResponse,
+} from "@bitwarden/common/billing/models/response/plan.response";
+
+import { PricingSummaryData } from "../shared/pricing-summary/pricing-summary.component";
+
+import { PricingSummaryService } from "./pricing-summary.service";
+
+describe("PricingSummaryService", () => {
+ let service: PricingSummaryService;
+
+ beforeEach(() => {
+ service = new PricingSummaryService();
+ });
+
+ describe("getPricingSummaryData", () => {
+ let mockPlan: PlanResponse;
+ let mockSub: OrganizationSubscriptionResponse;
+ let mockOrganization: Organization;
+
+ beforeEach(() => {
+ // Create mock plan with password manager features
+ mockPlan = {
+ productTier: ProductTierType.Teams,
+ PasswordManager: {
+ basePrice: 0,
+ seatPrice: 48,
+ baseSeats: 0,
+ hasAdditionalSeatsOption: true,
+ hasPremiumAccessOption: false,
+ premiumAccessOptionPrice: 0,
+ hasAdditionalStorageOption: true,
+ additionalStoragePricePerGb: 6,
+ baseStorageGb: 1,
+ } as PasswordManagerPlanFeaturesResponse,
+ SecretsManager: {
+ basePrice: 0,
+ seatPrice: 72,
+ baseSeats: 3,
+ hasAdditionalSeatsOption: true,
+ hasAdditionalServiceAccountOption: true,
+ additionalPricePerServiceAccount: 6,
+ baseServiceAccount: 50,
+ } as SecretsManagerPlanFeaturesResponse,
+ } as PlanResponse;
+
+ // Create mock subscription
+ mockSub = {
+ seats: 5,
+ smSeats: 5,
+ smServiceAccounts: 5,
+ maxStorageGb: 2,
+ customerDiscount: null,
+ } as OrganizationSubscriptionResponse;
+
+ // Create mock organization
+ mockOrganization = {
+ useSecretsManager: false,
+ } as Organization;
+ });
+
+ it("should calculate pricing data correctly for password manager only", async () => {
+ const result = await service.getPricingSummaryData(
+ mockPlan,
+ mockSub,
+ mockOrganization,
+ PlanInterval.Monthly,
+ false,
+ 50, // estimatedTax
+ );
+
+ expect(result).toEqual({
+ selectedPlanInterval: "month",
+ passwordManagerSeats: 5,
+ passwordManagerSeatTotal: 240, // 48 * 5
+ secretsManagerSeatTotal: 360, // 72 * 5
+ additionalStorageTotal: 6, // 6 * (2 - 1)
+ additionalStoragePriceMonthly: 6,
+ additionalServiceAccountTotal: 0, // No additional service accounts (50 base vs 5 used)
+ totalAppliedDiscount: 0,
+ secretsManagerSubtotal: 360, // 0 + 360 + 0
+ passwordManagerSubtotal: 246, // 0 + 240 + 6
+ total: 296, // 246 + 50 (tax) - organization doesn't use secrets manager
+ organization: mockOrganization,
+ sub: mockSub,
+ selectedPlan: mockPlan,
+ selectedInterval: PlanInterval.Monthly,
+ discountPercentageFromSub: 0,
+ discountPercentage: 20,
+ acceptingSponsorship: false,
+ additionalServiceAccount: 0, // 50 - 5 = 45, which is > 0, so return 0
+ storageGb: 1,
+ isSecretsManagerTrial: false,
+ estimatedTax: 50,
+ });
+ });
+
+ it("should calculate pricing data correctly with secrets manager enabled", async () => {
+ mockOrganization.useSecretsManager = true;
+
+ const result = await service.getPricingSummaryData(
+ mockPlan,
+ mockSub,
+ mockOrganization,
+ PlanInterval.Monthly,
+ false,
+ 50,
+ );
+
+ expect(result.total).toBe(656); // passwordManagerSubtotal (246) + secretsManagerSubtotal (360) + tax (50)
+ });
+
+ it("should handle secrets manager trial", async () => {
+ const result = await service.getPricingSummaryData(
+ mockPlan,
+ mockSub,
+ mockOrganization,
+ PlanInterval.Monthly,
+ true, // isSecretsManagerTrial
+ 50,
+ );
+
+ expect(result.passwordManagerSeatTotal).toBe(0); // Should be 0 during trial
+ expect(result.discountPercentageFromSub).toBe(0); // Should be 0 during trial
+ });
+
+ it("should handle premium access option", async () => {
+ mockPlan.PasswordManager.hasPremiumAccessOption = true;
+ mockPlan.PasswordManager.premiumAccessOptionPrice = 25;
+
+ const result = await service.getPricingSummaryData(
+ mockPlan,
+ mockSub,
+ mockOrganization,
+ PlanInterval.Monthly,
+ false,
+ 50,
+ );
+
+ expect(result.passwordManagerSubtotal).toBe(271); // 0 + 240 + 6 + 25
+ });
+
+ it("should handle customer discount", async () => {
+ mockSub.customerDiscount = {
+ id: "discount1",
+ active: true,
+ percentOff: 10,
+ appliesTo: ["subscription"],
+ } as BillingCustomerDiscount;
+
+ const result = await service.getPricingSummaryData(
+ mockPlan,
+ mockSub,
+ mockOrganization,
+ PlanInterval.Monthly,
+ false,
+ 50,
+ );
+
+ expect(result.discountPercentageFromSub).toBe(10);
+ });
+
+ it("should handle zero storage calculation", async () => {
+ mockSub.maxStorageGb = 1; // Same as base storage
+
+ const result = await service.getPricingSummaryData(
+ mockPlan,
+ mockSub,
+ mockOrganization,
+ PlanInterval.Monthly,
+ false,
+ 50,
+ );
+
+ expect(result.additionalStorageTotal).toBe(0);
+ expect(result.storageGb).toBe(0);
+ });
+ });
+
+ describe("getAdditionalServiceAccount", () => {
+ let mockPlan: PlanResponse;
+ let mockSub: OrganizationSubscriptionResponse;
+
+ beforeEach(() => {
+ mockPlan = {
+ SecretsManager: {
+ baseServiceAccount: 50,
+ } as SecretsManagerPlanFeaturesResponse,
+ } as PlanResponse;
+
+ mockSub = {
+ smServiceAccounts: 55,
+ } as OrganizationSubscriptionResponse;
+ });
+
+ it("should return additional service accounts when used exceeds base", () => {
+ const result = service.getAdditionalServiceAccount(mockPlan, mockSub);
+ expect(result).toBe(5); // Math.abs(50 - 55) = 5
+ });
+
+ it("should return 0 when used is less than or equal to base", () => {
+ mockSub.smServiceAccounts = 40;
+ const result = service.getAdditionalServiceAccount(mockPlan, mockSub);
+ expect(result).toBe(0);
+ });
+
+ it("should return 0 when used equals base", () => {
+ mockSub.smServiceAccounts = 50;
+ const result = service.getAdditionalServiceAccount(mockPlan, mockSub);
+ expect(result).toBe(0);
+ });
+
+ it("should return 0 when plan is null", () => {
+ const result = service.getAdditionalServiceAccount(null, mockSub);
+ expect(result).toBe(0);
+ });
+
+ it("should return 0 when plan has no SecretsManager", () => {
+ mockPlan.SecretsManager = null;
+ const result = service.getAdditionalServiceAccount(mockPlan, mockSub);
+ expect(result).toBe(0);
+ });
+ });
+});
diff --git a/apps/web/src/app/billing/services/pricing-summary.service.ts b/apps/web/src/app/billing/services/pricing-summary.service.ts
index b3c071a8b88..da2fe0e8dbb 100644
--- a/apps/web/src/app/billing/services/pricing-summary.service.ts
+++ b/apps/web/src/app/billing/services/pricing-summary.service.ts
@@ -31,9 +31,10 @@ export class PricingSummaryService {
const additionalServiceAccount = this.getAdditionalServiceAccount(plan, sub);
+ const storageGb = Math.max(0, (sub?.maxStorageGb ?? 0) - plan.PasswordManager.baseStorageGb);
+
const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption
- ? plan.PasswordManager.additionalStoragePricePerGb *
- (sub?.maxStorageGb ? sub.maxStorageGb - 1 : 0)
+ ? plan.PasswordManager.additionalStoragePricePerGb * storageGb
: 0;
const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0;
@@ -66,7 +67,6 @@ export class PricingSummaryService {
: (sub?.customerDiscount?.percentOff ?? 0);
const discountPercentage = 20;
const acceptingSponsorship = false;
- const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0;
const total = organization?.useSecretsManager
? passwordManagerSubtotal + secretsManagerSubtotal + estimatedTax
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 90468c61d5c..582efade7f4 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -3060,7 +3060,16 @@
"message": "Upgrade your account to a Premium membership and unlock some great additional features."
},
"premiumSignUpStorage": {
- "message": "1 GB encrypted storage for file attachments."
+ "message": "1 GB encrypted storage for file attachments."
+ },
+ "premiumSignUpStorageV2": {
+ "message": "$SIZE$ encrypted storage for file attachments.",
+ "placeholders": {
+ "size": {
+ "content": "$1",
+ "example": "1 GB"
+ }
+ }
},
"premiumSignUpTwoStepOptions": {
"message": "Proprietary two-step login options such as YubiKey and Duo."
diff --git a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts
index 107eb068e76..30a4d38b1df 100644
--- a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts
+++ b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts
@@ -40,6 +40,7 @@ describe("PremiumUpgradeDialogComponent", () => {
type: "standalone",
annualPrice: 10,
annualPricePerAdditionalStorageGB: 4,
+ providedStorageGB: 1,
features: [
{ key: "feature1", value: "Feature 1" },
{ key: "feature2", value: "Feature 2" },
@@ -58,6 +59,7 @@ describe("PremiumUpgradeDialogComponent", () => {
users: 6,
annualPrice: 40,
annualPricePerAdditionalStorageGB: 4,
+ providedStorageGB: 1,
features: [{ key: "featureA", value: "Feature A" }],
},
};
diff --git a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts
index 7ba09192d3c..7fd66878cae 100644
--- a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts
+++ b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts
@@ -31,6 +31,7 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = {
type: "standalone",
annualPrice: 10,
annualPricePerAdditionalStorageGB: 4,
+ providedStorageGB: 1,
features: [
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
{ key: "secureFileStorage", value: "Secure file storage" },
diff --git a/libs/angular/src/billing/components/premium.component.ts b/libs/angular/src/billing/components/premium.component.ts
index 6d0b90385ba..3f53d62e561 100644
--- a/libs/angular/src/billing/components/premium.component.ts
+++ b/libs/angular/src/billing/components/premium.component.ts
@@ -5,6 +5,7 @@ import { firstValueFrom, Observable, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -16,6 +17,7 @@ import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/com
export class PremiumComponent implements OnInit {
isPremium$: Observable;
price = 10;
+ storageProvidedGb = 0;
refreshPromise: Promise;
cloudWebVaultUrl: string;
@@ -29,6 +31,7 @@ export class PremiumComponent implements OnInit {
billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
accountService: AccountService,
+ private billingApiService: BillingApiServiceAbstraction,
) {
this.isPremium$ = accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -39,6 +42,9 @@ export class PremiumComponent implements OnInit {
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
+ const premiumResponse = await this.billingApiService.getPremiumPlan();
+ this.storageProvidedGb = premiumResponse.storage.provided;
+ this.price = premiumResponse.seat.price;
}
async refresh() {
diff --git a/libs/common/src/billing/models/response/premium-plan.response.ts b/libs/common/src/billing/models/response/premium-plan.response.ts
index f5df560a601..73e4f834c6f 100644
--- a/libs/common/src/billing/models/response/premium-plan.response.ts
+++ b/libs/common/src/billing/models/response/premium-plan.response.ts
@@ -4,10 +4,12 @@ export class PremiumPlanResponse extends BaseResponse {
seat: {
stripePriceId: string;
price: number;
+ provided: number;
};
storage: {
stripePriceId: string;
price: number;
+ provided: number;
};
constructor(response: any) {
@@ -30,6 +32,7 @@ export class PremiumPlanResponse extends BaseResponse {
class PurchasableResponse extends BaseResponse {
stripePriceId: string;
price: number;
+ provided: number;
constructor(response: any) {
super(response);
@@ -43,5 +46,9 @@ class PurchasableResponse extends BaseResponse {
if (typeof this.price !== "number" || isNaN(this.price)) {
throw new Error("PurchasableResponse: Missing or invalid 'Price' property");
}
+ this.provided = this.getResponseProperty("Provided");
+ if (typeof this.provided !== "number" || isNaN(this.provided)) {
+ throw new Error("PurchasableResponse: Missing or invalid 'Provided' property");
+ }
}
}
diff --git a/libs/common/src/billing/services/subscription-pricing.service.spec.ts b/libs/common/src/billing/services/subscription-pricing.service.spec.ts
index 8f5e9c0a3ab..76e15c646af 100644
--- a/libs/common/src/billing/services/subscription-pricing.service.spec.ts
+++ b/libs/common/src/billing/services/subscription-pricing.service.spec.ts
@@ -55,6 +55,7 @@ describe("DefaultSubscriptionPricingService", () => {
basePrice: 36,
seatPrice: 0,
additionalStoragePricePerGb: 4,
+ providedStorageGB: 1,
allowSeatAutoscale: false,
maxSeats: 6,
maxCollections: null,
@@ -94,6 +95,7 @@ describe("DefaultSubscriptionPricingService", () => {
basePrice: 0,
seatPrice: 36,
additionalStoragePricePerGb: 4,
+ providedStorageGB: 1,
allowSeatAutoscale: true,
maxSeats: null,
maxCollections: null,
@@ -359,6 +361,7 @@ describe("DefaultSubscriptionPricingService", () => {
type: "standalone",
annualPrice: 10,
annualPricePerAdditionalStorageGB: 4,
+ providedStorageGB: 1,
features: [
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
{ key: "secureFileStorage", value: "Secure file storage" },
@@ -383,6 +386,7 @@ describe("DefaultSubscriptionPricingService", () => {
annualPrice: mockFamiliesPlan.PasswordManager.basePrice,
annualPricePerAdditionalStorageGB:
mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: mockFamiliesPlan.PasswordManager.baseStorageGb,
features: [
{ key: "premiumAccounts", value: "6 premium accounts" },
{ key: "familiesUnlimitedSharing", value: "Unlimited sharing for families" },
@@ -456,6 +460,7 @@ describe("DefaultSubscriptionPricingService", () => {
expect(premiumTier.passwordManager.annualPrice).toEqual(10);
expect(premiumTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(4);
+ expect(premiumTier.passwordManager.providedStorageGB).toEqual(1);
expect(familiesTier.passwordManager.annualPrice).toEqual(
mockFamiliesPlan.PasswordManager.basePrice,
@@ -463,6 +468,9 @@ describe("DefaultSubscriptionPricingService", () => {
expect(familiesTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(
mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb,
);
+ expect(familiesTier.passwordManager.providedStorageGB).toEqual(
+ mockFamiliesPlan.PasswordManager.baseStorageGb,
+ );
done();
});
@@ -487,6 +495,7 @@ describe("DefaultSubscriptionPricingService", () => {
annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
mockTeamsPlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: mockTeamsPlan.PasswordManager.baseStorageGb,
features: [
{ key: "secureItemSharing", value: "Secure item sharing" },
{ key: "eventLogMonitoring", value: "Event log monitoring" },
@@ -522,6 +531,7 @@ describe("DefaultSubscriptionPricingService", () => {
annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: mockEnterprisePlan.PasswordManager.baseStorageGb,
features: [
{ key: "enterpriseSecurityPolicies", value: "Enterprise security policies" },
{ key: "passwordLessSso", value: "Passwordless SSO" },
@@ -648,6 +658,9 @@ describe("DefaultSubscriptionPricingService", () => {
expect(teamsSecretsManager.annualPricePerAdditionalServiceAccount).toEqual(
mockTeamsPlan.SecretsManager.additionalPricePerServiceAccount,
);
+ expect(teamsPasswordManager.providedStorageGB).toEqual(
+ mockTeamsPlan.PasswordManager.baseStorageGb,
+ );
const enterprisePasswordManager = enterpriseTier.passwordManager as any;
const enterpriseSecretsManager = enterpriseTier.secretsManager as any;
@@ -657,6 +670,9 @@ describe("DefaultSubscriptionPricingService", () => {
expect(enterprisePasswordManager.annualPricePerAdditionalStorageGB).toEqual(
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
);
+ expect(enterprisePasswordManager.providedStorageGB).toEqual(
+ mockEnterprisePlan.PasswordManager.baseStorageGb,
+ );
expect(enterpriseSecretsManager.annualPricePerUser).toEqual(
mockEnterprisePlan.SecretsManager.seatPrice,
);
@@ -729,6 +745,7 @@ describe("DefaultSubscriptionPricingService", () => {
annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
mockTeamsPlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: mockTeamsPlan.PasswordManager.baseStorageGb,
features: [
{ key: "secureItemSharing", value: "Secure item sharing" },
{ key: "eventLogMonitoring", value: "Event log monitoring" },
@@ -764,6 +781,7 @@ describe("DefaultSubscriptionPricingService", () => {
annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: mockEnterprisePlan.PasswordManager.baseStorageGb,
features: [
{ key: "enterpriseSecurityPolicies", value: "Enterprise security policies" },
{ key: "passwordLessSso", value: "Passwordless SSO" },
diff --git a/libs/common/src/billing/services/subscription-pricing.service.ts b/libs/common/src/billing/services/subscription-pricing.service.ts
index f1502eb26e8..a3f048fee78 100644
--- a/libs/common/src/billing/services/subscription-pricing.service.ts
+++ b/libs/common/src/billing/services/subscription-pricing.service.ts
@@ -40,6 +40,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
*/
private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10;
private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4;
+ private static readonly FALLBACK_PREMIUM_PROVIDED_STORAGE_GB = 1;
constructor(
private billingApiService: BillingApiServiceAbstraction,
@@ -114,11 +115,13 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
map((premiumPlan) => ({
seat: premiumPlan.seat.price,
storage: premiumPlan.storage.price,
+ provided: premiumPlan.storage.provided,
})),
)
: of({
seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE,
storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE,
+ provided: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_PROVIDED_STORAGE_GB,
}),
),
map((premiumPrices) => ({
@@ -130,6 +133,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
type: "standalone",
annualPrice: premiumPrices.seat,
annualPricePerAdditionalStorageGB: premiumPrices.storage,
+ providedStorageGB: premiumPrices.provided,
features: [
this.featureTranslations.builtInAuthenticator(),
this.featureTranslations.secureFileStorage(),
@@ -161,6 +165,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
annualPrice: familiesPlan.PasswordManager.basePrice,
annualPricePerAdditionalStorageGB:
familiesPlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: familiesPlan.PasswordManager.baseStorageGb,
features: [
this.featureTranslations.premiumAccounts(),
this.featureTranslations.familiesUnlimitedSharing(),
@@ -214,6 +219,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
annualPricePerUser: annualTeamsPlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
annualTeamsPlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: annualTeamsPlan.PasswordManager.baseStorageGb,
features: [
this.featureTranslations.secureItemSharing(),
this.featureTranslations.eventLogMonitoring(),
@@ -253,6 +259,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
annualPricePerUser: annualEnterprisePlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
annualEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
+ providedStorageGB: annualEnterprisePlan.PasswordManager.baseStorageGb,
features: [
this.featureTranslations.enterpriseSecurityPolicies(),
this.featureTranslations.passwordLessSso(),
diff --git a/libs/common/src/billing/types/subscription-pricing-tier.ts b/libs/common/src/billing/types/subscription-pricing-tier.ts
index 8febc4b86db..3f5c076ba4f 100644
--- a/libs/common/src/billing/types/subscription-pricing-tier.ts
+++ b/libs/common/src/billing/types/subscription-pricing-tier.ts
@@ -30,13 +30,19 @@ type HasAdditionalStorage = {
annualPricePerAdditionalStorageGB: number;
};
+type HasProvidedStorage = {
+ providedStorageGB: number;
+};
+
type StandalonePasswordManager = HasFeatures &
- HasAdditionalStorage & {
+ HasAdditionalStorage &
+ HasProvidedStorage & {
type: "standalone";
annualPrice: number;
};
type PackagedPasswordManager = HasFeatures &
+ HasProvidedStorage &
HasAdditionalStorage & {
type: "packaged";
users: number;
@@ -52,6 +58,7 @@ type CustomPasswordManager = HasFeatures & {
};
type ScalablePasswordManager = HasFeatures &
+ HasProvidedStorage &
HasAdditionalStorage & {
type: "scalable";
annualPricePerUser: number;