1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-27600] Replace Hard-Coded Storage amount (#17393)

* feat(billing): add provided as a required property to premium response

* fix(billing): replace hard coded storage variables with retrieved plan

* tests(billing): add tests to pricing-summary service

* feat(billing): add optional property.

* fix(billing): update storage logic

* fix(billing): remove optional check

* fix(billing): remove optionality

* fix(billing): remove optionality

* fix(billing): refactored storage calculation logic

* feat(billing): add provided amounts to subscription-pricing-service

* fix(billing): update cloud premium component

* fix(billing): update desktop premium component

* fix(billing): update org plans component

* fix(billing) update stories and tests

* fix(billing): update messages

* fix(billing): replace storage sizes

* fix(billing): update messages

* fix(billing): update components

* fix(billing): update components for pricing and storage retrieval

* fix(billing): revert self-hosted change
This commit is contained in:
Stephon Brown
2025-12-02 10:49:55 -05:00
committed by GitHub
parent 049acf1e12
commit a9bf66e689
20 changed files with 343 additions and 24 deletions

View File

@@ -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" }],
},
};

View File

@@ -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" },

View File

@@ -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<boolean>;
price = 10;
storageProvidedGb = 0;
refreshPromise: Promise<any>;
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() {

View File

@@ -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");
}
}
}

View File

@@ -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" },

View File

@@ -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(),

View File

@@ -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;