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:
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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" }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user