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

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

View File

@@ -12,7 +12,7 @@
<div class="tw-flex tw-flex-col tw-p-2">
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
<li>
{{ "ppremiumSignUpStorage" | i18n }}
{{ "premiumSignUpStorageV2" | i18n: `${storageProvidedGb} GB` }}
</li>
<li>
{{ "premiumSignUpTwoStepOptions" | i18n }}

View File

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

View File

@@ -13,7 +13,7 @@
<ul class="bwi-ul">
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
{{ "premiumSignUpStorageV2" | i18n: `${storageProvidedGb} GB` }}
</li>
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>

View File

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

View File

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

View File

@@ -24,7 +24,7 @@
<ul class="bwi-ul">
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
{{ "premiumSignUpStorageV2" | i18n: `${(providedStorageGb$ | async)} GB` }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
@@ -82,7 +82,10 @@
/>
<bit-hint>{{
"additionalStorageIntervalDesc"
| i18n: "1 GB" : (storagePrice$ | async | currency: "$") : ("year" | i18n)
| i18n
: `${(providedStorageGb$ | async)} GB`
: (storagePrice$ | async | currency: "$")
: ("year" | i18n)
}}</bit-hint>
</bit-form-field>
</div>

View File

@@ -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) =>

View File

@@ -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) {

View File

@@ -104,7 +104,7 @@
<li *ngIf="selectableProduct.PasswordManager.baseStorageGb">
{{
"gbEncryptedFileStorage"
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
| i18n: selectableProduct.PasswordManager.baseStorageGb + " GB"
}}
</li>
<li *ngIf="selectableProduct.hasGroups">
@@ -239,7 +239,7 @@
<bit-hint class="tw-text-sm">{{
"additionalStorageIntervalDesc"
| i18n
: "1 GB"
: `${selectedPlan.PasswordManager.baseStorageGb} GB`
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
: ("month" | i18n)
}}</bit-hint>

View File

@@ -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<PricingSummaryData>({
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);
});
});
});

View File

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

View File

@@ -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."

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;