mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +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": {
|
"ppremiumSignUpStorage": {
|
||||||
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"premiumSignUpEmergency": {
|
"premiumSignUpEmergency": {
|
||||||
"message": "Emergency access."
|
"message": "Emergency access."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="tw-flex tw-flex-col tw-p-2">
|
<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">
|
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
|
||||||
<li>
|
<li>
|
||||||
{{ "ppremiumSignUpStorage" | i18n }}
|
{{ "premiumSignUpStorageV2" | i18n: `${storageProvidedGb} GB` }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule, CurrencyPipe, Location } from "@angular/common";
|
import { CommonModule, CurrencyPipe, Location } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/billing/components/premium.component";
|
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/billing/components/premium.component";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -44,7 +45,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
SectionComponent,
|
SectionComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PremiumV2Component extends BasePremiumComponent {
|
export class PremiumV2Component extends BasePremiumComponent implements OnInit {
|
||||||
priceString: string;
|
priceString: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -59,6 +60,7 @@ export class PremiumV2Component extends BasePremiumComponent {
|
|||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
billingApiService: BillingApiServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
@@ -70,15 +72,18 @@ export class PremiumV2Component extends BasePremiumComponent {
|
|||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
toastService,
|
toastService,
|
||||||
accountService,
|
accountService,
|
||||||
|
billingApiService,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
async ngOnInit() {
|
||||||
|
await super.ngOnInit();
|
||||||
// Support old price string. Can be removed in future once all translations are properly updated.
|
// Support old price string. Can be removed in future once all translations are properly updated.
|
||||||
const thePrice = this.currencyPipe.transform(this.price, "$");
|
const thePrice = this.currencyPipe.transform(this.price, "$");
|
||||||
// Safari extension crashes due to $1 appearing in the price string ($10.00). Escape the $ to fix.
|
// Safari extension crashes due to $1 appearing in the price string ($10.00). Escape the $ to fix.
|
||||||
const formattedPrice = this.platformUtilsService.isSafari()
|
const formattedPrice = this.platformUtilsService.isSafari()
|
||||||
? thePrice.replace("$", "$$$")
|
? thePrice.replace("$", "$$$")
|
||||||
: thePrice;
|
: thePrice;
|
||||||
this.priceString = i18nService.t("premiumPriceV2", formattedPrice);
|
this.priceString = this.i18nService.t("premiumPriceV2", formattedPrice);
|
||||||
if (this.priceString.indexOf("%price%") > -1) {
|
if (this.priceString.indexOf("%price%") > -1) {
|
||||||
this.priceString = this.priceString.replace("%price%", thePrice);
|
this.priceString = this.priceString.replace("%price%", thePrice);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<ul class="bwi-ul">
|
<ul class="bwi-ul">
|
||||||
<li>
|
<li>
|
||||||
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
|
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
|
||||||
{{ "premiumSignUpStorage" | i18n }}
|
{{ "premiumSignUpStorageV2" | i18n: `${storageProvidedGb} GB` }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
|
<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 { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/billing/components/premium.component";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -28,6 +29,7 @@ export class PremiumComponent extends BasePremiumComponent {
|
|||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
billingApiService: BillingApiServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
@@ -39,6 +41,7 @@ export class PremiumComponent extends BasePremiumComponent {
|
|||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
toastService,
|
toastService,
|
||||||
accountService,
|
accountService,
|
||||||
|
billingApiService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1490,6 +1490,15 @@
|
|||||||
"premiumSignUpStorage": {
|
"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": {
|
"premiumSignUpTwoStepOptions": {
|
||||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<ul class="bwi-ul">
|
<ul class="bwi-ul">
|
||||||
<li>
|
<li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
{{ "premiumSignUpStorage" | i18n }}
|
{{ "premiumSignUpStorageV2" | i18n: `${(providedStorageGb$ | async)} GB` }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
@@ -82,7 +82,10 @@
|
|||||||
/>
|
/>
|
||||||
<bit-hint>{{
|
<bit-hint>{{
|
||||||
"additionalStorageIntervalDesc"
|
"additionalStorageIntervalDesc"
|
||||||
| i18n: "1 GB" : (storagePrice$ | async | currency: "$") : ("year" | i18n)
|
| i18n
|
||||||
|
: `${(providedStorageGb$ | async)} GB`
|
||||||
|
: (storagePrice$ | async | currency: "$")
|
||||||
|
: ("year" | i18n)
|
||||||
}}</bit-hint>
|
}}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import { debounceTime } from "rxjs/operators";
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
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 { 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 { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -75,6 +75,7 @@ export class CloudHostedPremiumComponent {
|
|||||||
return {
|
return {
|
||||||
seat: premiumPlan.passwordManager.annualPrice,
|
seat: premiumPlan.passwordManager.annualPrice,
|
||||||
storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB,
|
storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB,
|
||||||
|
providedStorageGb: premiumPlan.passwordManager.providedStorageGB,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
@@ -84,6 +85,8 @@ export class CloudHostedPremiumComponent {
|
|||||||
|
|
||||||
storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage));
|
storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage));
|
||||||
|
|
||||||
|
providedStorageGb$ = this.premiumPrices$.pipe(map((prices) => prices.providedStorageGb));
|
||||||
|
|
||||||
protected isLoadingPrices$ = this.premiumPrices$.pipe(
|
protected isLoadingPrices$ = this.premiumPrices$.pipe(
|
||||||
map(() => false),
|
map(() => false),
|
||||||
startWith(true),
|
startWith(true),
|
||||||
@@ -134,7 +137,7 @@ export class CloudHostedPremiumComponent {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private subscriberBillingClient: SubscriberBillingClient,
|
private subscriberBillingClient: SubscriberBillingClient,
|
||||||
private taxClient: TaxClient,
|
private taxClient: TaxClient,
|
||||||
private subscriptionPricingService: DefaultSubscriptionPricingService,
|
private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((account) =>
|
||||||
|
|||||||
@@ -620,7 +620,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get storageGb() {
|
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 {
|
passwordManagerSeatTotal(plan: PlanResponse): number {
|
||||||
@@ -644,12 +647,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return plan.PasswordManager.additionalStoragePricePerGb * this.storageGb;
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
|
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
|
||||||
|
|||||||
@@ -239,7 +239,7 @@
|
|||||||
<bit-hint class="tw-text-sm">{{
|
<bit-hint class="tw-text-sm">{{
|
||||||
"additionalStorageIntervalDesc"
|
"additionalStorageIntervalDesc"
|
||||||
| i18n
|
| i18n
|
||||||
: "1 GB"
|
: `${selectedPlan.PasswordManager.baseStorageGb} GB`
|
||||||
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
||||||
: ("month" | i18n)
|
: ("month" | i18n)
|
||||||
}}</bit-hint>
|
}}</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 additionalServiceAccount = this.getAdditionalServiceAccount(plan, sub);
|
||||||
|
|
||||||
|
const storageGb = Math.max(0, (sub?.maxStorageGb ?? 0) - plan.PasswordManager.baseStorageGb);
|
||||||
|
|
||||||
const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption
|
const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption
|
||||||
? plan.PasswordManager.additionalStoragePricePerGb *
|
? plan.PasswordManager.additionalStoragePricePerGb * storageGb
|
||||||
(sub?.maxStorageGb ? sub.maxStorageGb - 1 : 0)
|
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0;
|
const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0;
|
||||||
@@ -66,7 +67,6 @@ export class PricingSummaryService {
|
|||||||
: (sub?.customerDiscount?.percentOff ?? 0);
|
: (sub?.customerDiscount?.percentOff ?? 0);
|
||||||
const discountPercentage = 20;
|
const discountPercentage = 20;
|
||||||
const acceptingSponsorship = false;
|
const acceptingSponsorship = false;
|
||||||
const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0;
|
|
||||||
|
|
||||||
const total = organization?.useSecretsManager
|
const total = organization?.useSecretsManager
|
||||||
? passwordManagerSubtotal + secretsManagerSubtotal + estimatedTax
|
? passwordManagerSubtotal + secretsManagerSubtotal + estimatedTax
|
||||||
|
|||||||
@@ -3062,6 +3062,15 @@
|
|||||||
"premiumSignUpStorage": {
|
"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": {
|
"premiumSignUpTwoStepOptions": {
|
||||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe("PremiumUpgradeDialogComponent", () => {
|
|||||||
type: "standalone",
|
type: "standalone",
|
||||||
annualPrice: 10,
|
annualPrice: 10,
|
||||||
annualPricePerAdditionalStorageGB: 4,
|
annualPricePerAdditionalStorageGB: 4,
|
||||||
|
providedStorageGB: 1,
|
||||||
features: [
|
features: [
|
||||||
{ key: "feature1", value: "Feature 1" },
|
{ key: "feature1", value: "Feature 1" },
|
||||||
{ key: "feature2", value: "Feature 2" },
|
{ key: "feature2", value: "Feature 2" },
|
||||||
@@ -58,6 +59,7 @@ describe("PremiumUpgradeDialogComponent", () => {
|
|||||||
users: 6,
|
users: 6,
|
||||||
annualPrice: 40,
|
annualPrice: 40,
|
||||||
annualPricePerAdditionalStorageGB: 4,
|
annualPricePerAdditionalStorageGB: 4,
|
||||||
|
providedStorageGB: 1,
|
||||||
features: [{ key: "featureA", value: "Feature A" }],
|
features: [{ key: "featureA", value: "Feature A" }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = {
|
|||||||
type: "standalone",
|
type: "standalone",
|
||||||
annualPrice: 10,
|
annualPrice: 10,
|
||||||
annualPricePerAdditionalStorageGB: 4,
|
annualPricePerAdditionalStorageGB: 4,
|
||||||
|
providedStorageGB: 1,
|
||||||
features: [
|
features: [
|
||||||
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
||||||
{ key: "secureFileStorage", value: "Secure file storage" },
|
{ 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 {
|
export class PremiumComponent implements OnInit {
|
||||||
isPremium$: Observable<boolean>;
|
isPremium$: Observable<boolean>;
|
||||||
price = 10;
|
price = 10;
|
||||||
|
storageProvidedGb = 0;
|
||||||
refreshPromise: Promise<any>;
|
refreshPromise: Promise<any>;
|
||||||
cloudWebVaultUrl: string;
|
cloudWebVaultUrl: string;
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ export class PremiumComponent implements OnInit {
|
|||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
this.isPremium$ = accountService.activeAccount$.pipe(
|
this.isPremium$ = accountService.activeAccount$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((account) =>
|
||||||
@@ -39,6 +42,9 @@ export class PremiumComponent implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
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() {
|
async refresh() {
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ export class PremiumPlanResponse extends BaseResponse {
|
|||||||
seat: {
|
seat: {
|
||||||
stripePriceId: string;
|
stripePriceId: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
provided: number;
|
||||||
};
|
};
|
||||||
storage: {
|
storage: {
|
||||||
stripePriceId: string;
|
stripePriceId: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
provided: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
@@ -30,6 +32,7 @@ export class PremiumPlanResponse extends BaseResponse {
|
|||||||
class PurchasableResponse extends BaseResponse {
|
class PurchasableResponse extends BaseResponse {
|
||||||
stripePriceId: string;
|
stripePriceId: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
provided: number;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@@ -43,5 +46,9 @@ class PurchasableResponse extends BaseResponse {
|
|||||||
if (typeof this.price !== "number" || isNaN(this.price)) {
|
if (typeof this.price !== "number" || isNaN(this.price)) {
|
||||||
throw new Error("PurchasableResponse: Missing or invalid 'Price' property");
|
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,
|
basePrice: 36,
|
||||||
seatPrice: 0,
|
seatPrice: 0,
|
||||||
additionalStoragePricePerGb: 4,
|
additionalStoragePricePerGb: 4,
|
||||||
|
providedStorageGB: 1,
|
||||||
allowSeatAutoscale: false,
|
allowSeatAutoscale: false,
|
||||||
maxSeats: 6,
|
maxSeats: 6,
|
||||||
maxCollections: null,
|
maxCollections: null,
|
||||||
@@ -94,6 +95,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
basePrice: 0,
|
basePrice: 0,
|
||||||
seatPrice: 36,
|
seatPrice: 36,
|
||||||
additionalStoragePricePerGb: 4,
|
additionalStoragePricePerGb: 4,
|
||||||
|
providedStorageGB: 1,
|
||||||
allowSeatAutoscale: true,
|
allowSeatAutoscale: true,
|
||||||
maxSeats: null,
|
maxSeats: null,
|
||||||
maxCollections: null,
|
maxCollections: null,
|
||||||
@@ -359,6 +361,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
type: "standalone",
|
type: "standalone",
|
||||||
annualPrice: 10,
|
annualPrice: 10,
|
||||||
annualPricePerAdditionalStorageGB: 4,
|
annualPricePerAdditionalStorageGB: 4,
|
||||||
|
providedStorageGB: 1,
|
||||||
features: [
|
features: [
|
||||||
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
||||||
{ key: "secureFileStorage", value: "Secure file storage" },
|
{ key: "secureFileStorage", value: "Secure file storage" },
|
||||||
@@ -383,6 +386,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
annualPrice: mockFamiliesPlan.PasswordManager.basePrice,
|
annualPrice: mockFamiliesPlan.PasswordManager.basePrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb,
|
mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: mockFamiliesPlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
{ key: "premiumAccounts", value: "6 premium accounts" },
|
{ key: "premiumAccounts", value: "6 premium accounts" },
|
||||||
{ key: "familiesUnlimitedSharing", value: "Unlimited sharing for families" },
|
{ key: "familiesUnlimitedSharing", value: "Unlimited sharing for families" },
|
||||||
@@ -456,6 +460,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
|
|
||||||
expect(premiumTier.passwordManager.annualPrice).toEqual(10);
|
expect(premiumTier.passwordManager.annualPrice).toEqual(10);
|
||||||
expect(premiumTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(4);
|
expect(premiumTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(4);
|
||||||
|
expect(premiumTier.passwordManager.providedStorageGB).toEqual(1);
|
||||||
|
|
||||||
expect(familiesTier.passwordManager.annualPrice).toEqual(
|
expect(familiesTier.passwordManager.annualPrice).toEqual(
|
||||||
mockFamiliesPlan.PasswordManager.basePrice,
|
mockFamiliesPlan.PasswordManager.basePrice,
|
||||||
@@ -463,6 +468,9 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
expect(familiesTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(
|
expect(familiesTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(
|
||||||
mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb,
|
mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
);
|
);
|
||||||
|
expect(familiesTier.passwordManager.providedStorageGB).toEqual(
|
||||||
|
mockFamiliesPlan.PasswordManager.baseStorageGb,
|
||||||
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -487,6 +495,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice,
|
annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
mockTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
mockTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: mockTeamsPlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
{ key: "secureItemSharing", value: "Secure item sharing" },
|
{ key: "secureItemSharing", value: "Secure item sharing" },
|
||||||
{ key: "eventLogMonitoring", value: "Event log monitoring" },
|
{ key: "eventLogMonitoring", value: "Event log monitoring" },
|
||||||
@@ -522,6 +531,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice,
|
annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: mockEnterprisePlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
{ key: "enterpriseSecurityPolicies", value: "Enterprise security policies" },
|
{ key: "enterpriseSecurityPolicies", value: "Enterprise security policies" },
|
||||||
{ key: "passwordLessSso", value: "Passwordless SSO" },
|
{ key: "passwordLessSso", value: "Passwordless SSO" },
|
||||||
@@ -648,6 +658,9 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
expect(teamsSecretsManager.annualPricePerAdditionalServiceAccount).toEqual(
|
expect(teamsSecretsManager.annualPricePerAdditionalServiceAccount).toEqual(
|
||||||
mockTeamsPlan.SecretsManager.additionalPricePerServiceAccount,
|
mockTeamsPlan.SecretsManager.additionalPricePerServiceAccount,
|
||||||
);
|
);
|
||||||
|
expect(teamsPasswordManager.providedStorageGB).toEqual(
|
||||||
|
mockTeamsPlan.PasswordManager.baseStorageGb,
|
||||||
|
);
|
||||||
|
|
||||||
const enterprisePasswordManager = enterpriseTier.passwordManager as any;
|
const enterprisePasswordManager = enterpriseTier.passwordManager as any;
|
||||||
const enterpriseSecretsManager = enterpriseTier.secretsManager as any;
|
const enterpriseSecretsManager = enterpriseTier.secretsManager as any;
|
||||||
@@ -657,6 +670,9 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
expect(enterprisePasswordManager.annualPricePerAdditionalStorageGB).toEqual(
|
expect(enterprisePasswordManager.annualPricePerAdditionalStorageGB).toEqual(
|
||||||
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
);
|
);
|
||||||
|
expect(enterprisePasswordManager.providedStorageGB).toEqual(
|
||||||
|
mockEnterprisePlan.PasswordManager.baseStorageGb,
|
||||||
|
);
|
||||||
expect(enterpriseSecretsManager.annualPricePerUser).toEqual(
|
expect(enterpriseSecretsManager.annualPricePerUser).toEqual(
|
||||||
mockEnterprisePlan.SecretsManager.seatPrice,
|
mockEnterprisePlan.SecretsManager.seatPrice,
|
||||||
);
|
);
|
||||||
@@ -729,6 +745,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice,
|
annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
mockTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
mockTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: mockTeamsPlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
{ key: "secureItemSharing", value: "Secure item sharing" },
|
{ key: "secureItemSharing", value: "Secure item sharing" },
|
||||||
{ key: "eventLogMonitoring", value: "Event log monitoring" },
|
{ key: "eventLogMonitoring", value: "Event log monitoring" },
|
||||||
@@ -764,6 +781,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice,
|
annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: mockEnterprisePlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
{ key: "enterpriseSecurityPolicies", value: "Enterprise security policies" },
|
{ key: "enterpriseSecurityPolicies", value: "Enterprise security policies" },
|
||||||
{ key: "passwordLessSso", value: "Passwordless SSO" },
|
{ 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_SEAT_PRICE = 10;
|
||||||
private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4;
|
private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4;
|
||||||
|
private static readonly FALLBACK_PREMIUM_PROVIDED_STORAGE_GB = 1;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private billingApiService: BillingApiServiceAbstraction,
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
@@ -114,11 +115,13 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
map((premiumPlan) => ({
|
map((premiumPlan) => ({
|
||||||
seat: premiumPlan.seat.price,
|
seat: premiumPlan.seat.price,
|
||||||
storage: premiumPlan.storage.price,
|
storage: premiumPlan.storage.price,
|
||||||
|
provided: premiumPlan.storage.provided,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
: of({
|
: of({
|
||||||
seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE,
|
seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE,
|
||||||
storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE,
|
storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE,
|
||||||
|
provided: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_PROVIDED_STORAGE_GB,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
map((premiumPrices) => ({
|
map((premiumPrices) => ({
|
||||||
@@ -130,6 +133,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
type: "standalone",
|
type: "standalone",
|
||||||
annualPrice: premiumPrices.seat,
|
annualPrice: premiumPrices.seat,
|
||||||
annualPricePerAdditionalStorageGB: premiumPrices.storage,
|
annualPricePerAdditionalStorageGB: premiumPrices.storage,
|
||||||
|
providedStorageGB: premiumPrices.provided,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.builtInAuthenticator(),
|
this.featureTranslations.builtInAuthenticator(),
|
||||||
this.featureTranslations.secureFileStorage(),
|
this.featureTranslations.secureFileStorage(),
|
||||||
@@ -161,6 +165,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
annualPrice: familiesPlan.PasswordManager.basePrice,
|
annualPrice: familiesPlan.PasswordManager.basePrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
familiesPlan.PasswordManager.additionalStoragePricePerGb,
|
familiesPlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: familiesPlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.premiumAccounts(),
|
this.featureTranslations.premiumAccounts(),
|
||||||
this.featureTranslations.familiesUnlimitedSharing(),
|
this.featureTranslations.familiesUnlimitedSharing(),
|
||||||
@@ -214,6 +219,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
annualPricePerUser: annualTeamsPlan.PasswordManager.seatPrice,
|
annualPricePerUser: annualTeamsPlan.PasswordManager.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
annualTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
annualTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: annualTeamsPlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.secureItemSharing(),
|
this.featureTranslations.secureItemSharing(),
|
||||||
this.featureTranslations.eventLogMonitoring(),
|
this.featureTranslations.eventLogMonitoring(),
|
||||||
@@ -253,6 +259,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
annualPricePerUser: annualEnterprisePlan.PasswordManager.seatPrice,
|
annualPricePerUser: annualEnterprisePlan.PasswordManager.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
annualEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
annualEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
||||||
|
providedStorageGB: annualEnterprisePlan.PasswordManager.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.enterpriseSecurityPolicies(),
|
this.featureTranslations.enterpriseSecurityPolicies(),
|
||||||
this.featureTranslations.passwordLessSso(),
|
this.featureTranslations.passwordLessSso(),
|
||||||
|
|||||||
@@ -30,13 +30,19 @@ type HasAdditionalStorage = {
|
|||||||
annualPricePerAdditionalStorageGB: number;
|
annualPricePerAdditionalStorageGB: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HasProvidedStorage = {
|
||||||
|
providedStorageGB: number;
|
||||||
|
};
|
||||||
|
|
||||||
type StandalonePasswordManager = HasFeatures &
|
type StandalonePasswordManager = HasFeatures &
|
||||||
HasAdditionalStorage & {
|
HasAdditionalStorage &
|
||||||
|
HasProvidedStorage & {
|
||||||
type: "standalone";
|
type: "standalone";
|
||||||
annualPrice: number;
|
annualPrice: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PackagedPasswordManager = HasFeatures &
|
type PackagedPasswordManager = HasFeatures &
|
||||||
|
HasProvidedStorage &
|
||||||
HasAdditionalStorage & {
|
HasAdditionalStorage & {
|
||||||
type: "packaged";
|
type: "packaged";
|
||||||
users: number;
|
users: number;
|
||||||
@@ -52,6 +58,7 @@ type CustomPasswordManager = HasFeatures & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ScalablePasswordManager = HasFeatures &
|
type ScalablePasswordManager = HasFeatures &
|
||||||
|
HasProvidedStorage &
|
||||||
HasAdditionalStorage & {
|
HasAdditionalStorage & {
|
||||||
type: "scalable";
|
type: "scalable";
|
||||||
annualPricePerUser: number;
|
annualPricePerUser: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user