mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build
This commit is contained in:
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: 'Run stale action'
|
- name: 'Run stale action'
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
stale-issue-label: 'needs-reply'
|
stale-issue-label: 'needs-reply'
|
||||||
stale-pr-label: 'needs-changes'
|
stale-pr-label: 'needs-changes'
|
||||||
|
|||||||
8
apps/desktop/desktop_native/Cargo.lock
generated
8
apps/desktop/desktop_native/Cargo.lock
generated
@@ -1675,9 +1675,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.177"
|
version = "0.2.178"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
@@ -2877,9 +2877,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "3.5.0"
|
version = "3.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a"
|
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ futures = "=0.3.31"
|
|||||||
hex = "=0.4.3"
|
hex = "=0.4.3"
|
||||||
homedir = "=0.3.4"
|
homedir = "=0.3.4"
|
||||||
interprocess = "=2.2.1"
|
interprocess = "=2.2.1"
|
||||||
libc = "=0.2.177"
|
libc = "=0.2.178"
|
||||||
linux-keyutils = "=0.2.4"
|
linux-keyutils = "=0.2.4"
|
||||||
memsec = "=0.7.0"
|
memsec = "=0.7.0"
|
||||||
napi = "=2.16.17"
|
napi = "=2.16.17"
|
||||||
@@ -53,7 +53,7 @@ rsa = "=0.9.6"
|
|||||||
russh-cryptovec = "=0.7.3"
|
russh-cryptovec = "=0.7.3"
|
||||||
scopeguard = "=1.2.0"
|
scopeguard = "=1.2.0"
|
||||||
secmem-proc = "=0.3.7"
|
secmem-proc = "=0.3.7"
|
||||||
security-framework = "=3.5.0"
|
security-framework = "=3.5.1"
|
||||||
security-framework-sys = "=2.15.0"
|
security-framework-sys = "=2.15.0"
|
||||||
serde = "=1.0.209"
|
serde = "=1.0.209"
|
||||||
serde_json = "=1.0.127"
|
serde_json = "=1.0.127"
|
||||||
|
|||||||
@@ -37,6 +37,6 @@ concurrently(
|
|||||||
{
|
{
|
||||||
prefix: "name",
|
prefix: "name",
|
||||||
outputStream: process.stdout,
|
outputStream: process.stdout,
|
||||||
killOthers: ["success", "failure"],
|
killOthersOn: ["success", "failure"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ concurrently(
|
|||||||
{
|
{
|
||||||
prefix: "name",
|
prefix: "name",
|
||||||
outputStream: process.stdout,
|
outputStream: process.stdout,
|
||||||
killOthers: ["success", "failure"],
|
killOthersOn: ["success", "failure"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export class CloudHostedPremiumVNextComponent {
|
|||||||
return {
|
return {
|
||||||
tier,
|
tier,
|
||||||
price:
|
price:
|
||||||
tier?.passwordManager.type === "standalone"
|
tier?.passwordManager.type === "standalone" && tier.passwordManager.annualPrice
|
||||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||||
: 0,
|
: 0,
|
||||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||||
@@ -172,7 +172,7 @@ export class CloudHostedPremiumVNextComponent {
|
|||||||
return {
|
return {
|
||||||
tier,
|
tier,
|
||||||
price:
|
price:
|
||||||
tier?.passwordManager.type === "packaged"
|
tier?.passwordManager.type === "packaged" && tier.passwordManager.annualPrice
|
||||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||||
: 0,
|
: 0,
|
||||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, DestroyRef, OnInit, computed, input, output, signal } from "@angular/core";
|
import { Component, computed, DestroyRef, input, OnInit, output, signal } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { catchError, of } from "rxjs";
|
import { catchError, of } from "rxjs";
|
||||||
|
|
||||||
|
import { SubscriptionPricingCardDetails } from "@bitwarden/angular/billing/types/subscription-pricing-card-details";
|
||||||
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
||||||
import {
|
import {
|
||||||
PersonalSubscriptionPricingTier,
|
PersonalSubscriptionPricingTier,
|
||||||
PersonalSubscriptionPricingTierId,
|
PersonalSubscriptionPricingTierId,
|
||||||
PersonalSubscriptionPricingTierIds,
|
PersonalSubscriptionPricingTierIds,
|
||||||
SubscriptionCadence,
|
|
||||||
SubscriptionCadenceIds,
|
SubscriptionCadenceIds,
|
||||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -32,14 +32,6 @@ export type UpgradeAccountResult = {
|
|||||||
plan: PersonalSubscriptionPricingTierId | null;
|
plan: PersonalSubscriptionPricingTierId | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CardDetails = {
|
|
||||||
title: string;
|
|
||||||
tagline: string;
|
|
||||||
price: { amount: number; cadence: SubscriptionCadence };
|
|
||||||
button: { text: string; type: ButtonType };
|
|
||||||
features: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||||
@Component({
|
@Component({
|
||||||
@@ -60,8 +52,8 @@ export class UpgradeAccountComponent implements OnInit {
|
|||||||
planSelected = output<PersonalSubscriptionPricingTierId>();
|
planSelected = output<PersonalSubscriptionPricingTierId>();
|
||||||
closeClicked = output<UpgradeAccountStatus>();
|
closeClicked = output<UpgradeAccountStatus>();
|
||||||
protected readonly loading = signal(true);
|
protected readonly loading = signal(true);
|
||||||
protected premiumCardDetails!: CardDetails;
|
protected premiumCardDetails!: SubscriptionPricingCardDetails;
|
||||||
protected familiesCardDetails!: CardDetails;
|
protected familiesCardDetails!: SubscriptionPricingCardDetails;
|
||||||
|
|
||||||
protected familiesPlanType = PersonalSubscriptionPricingTierIds.Families;
|
protected familiesPlanType = PersonalSubscriptionPricingTierIds.Families;
|
||||||
protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium;
|
protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium;
|
||||||
@@ -122,14 +114,16 @@ export class UpgradeAccountComponent implements OnInit {
|
|||||||
private createCardDetails(
|
private createCardDetails(
|
||||||
tier: PersonalSubscriptionPricingTier,
|
tier: PersonalSubscriptionPricingTier,
|
||||||
buttonType: ButtonType,
|
buttonType: ButtonType,
|
||||||
): CardDetails {
|
): SubscriptionPricingCardDetails {
|
||||||
return {
|
return {
|
||||||
title: tier.name,
|
title: tier.name,
|
||||||
tagline: tier.description,
|
tagline: tier.description,
|
||||||
price: {
|
price: tier.passwordManager.annualPrice
|
||||||
|
? {
|
||||||
amount: tier.passwordManager.annualPrice / 12,
|
amount: tier.passwordManager.annualPrice / 12,
|
||||||
cadence: SubscriptionCadenceIds.Monthly,
|
cadence: SubscriptionCadenceIds.Monthly,
|
||||||
},
|
}
|
||||||
|
: undefined,
|
||||||
button: {
|
button: {
|
||||||
text: this.i18nService.t(
|
text: this.i18nService.t(
|
||||||
this.isFamiliesPlan(tier.id) ? "startFreeFamiliesTrial" : "upgradeToPremium",
|
this.isFamiliesPlan(tier.id) ? "startFreeFamiliesTrial" : "upgradeToPremium",
|
||||||
|
|||||||
@@ -200,7 +200,8 @@ export class UpgradePaymentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getPasswordManagerSeats(planDetails: PlanDetails): number {
|
private getPasswordManagerSeats(planDetails: PlanDetails): number {
|
||||||
return "users" in planDetails.details.passwordManager
|
return "users" in planDetails.details.passwordManager &&
|
||||||
|
planDetails.details.passwordManager.users
|
||||||
? planDetails.details.passwordManager.users
|
? planDetails.details.passwordManager.users
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,21 +20,22 @@
|
|||||||
<div
|
<div
|
||||||
class="tw-box-border tw-bg-background tw-text-main tw-size-full tw-flex tw-flex-col tw-px-8 tw-pb-2 tw-w-full tw-max-w-md"
|
class="tw-box-border tw-bg-background tw-text-main tw-size-full tw-flex tw-flex-col tw-px-8 tw-pb-2 tw-w-full tw-max-w-md"
|
||||||
>
|
>
|
||||||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-2">
|
<div class="tw-flex tw-items-center tw-justify-between">
|
||||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">
|
<h3 slot="title" class="tw-m-0" bitTypography="h3">
|
||||||
{{ "upgradeToPremium" | i18n }}
|
{{ "upgradeToPremium" | i18n }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tagline with consistent height (exactly 2 lines) -->
|
<!-- Tagline with consistent height (exactly 2 lines) -->
|
||||||
<div class="tw-mb-6 tw-h-6">
|
<div class="tw-h-6">
|
||||||
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
||||||
{{ cardDetails.tagline }}
|
{{ cardDetails.tagline }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Price Section -->
|
<!-- Price Section -->
|
||||||
<div class="tw-mb-6">
|
@if (cardDetails.price) {
|
||||||
|
<div class="tw-mt-5">
|
||||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||||
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
|
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
|
||||||
cardDetails.price.amount | currency: "$"
|
cardDetails.price.amount | currency: "$"
|
||||||
@@ -44,9 +45,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Button space (always reserved) -->
|
<!-- Button space (always reserved) -->
|
||||||
<div class="tw-mb-6 tw-h-12">
|
<div class="tw-my-5 tw-h-12">
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
[buttonType]="cardDetails.button.type"
|
[buttonType]="cardDetails.button.type"
|
||||||
|
|||||||
@@ -206,4 +206,39 @@ describe("PremiumUpgradeDialogComponent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("self-hosted environment", () => {
|
||||||
|
it("should handle null price data for self-hosted environment", async () => {
|
||||||
|
const selfHostedPremiumTier: PersonalSubscriptionPricingTier = {
|
||||||
|
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||||
|
name: "Premium",
|
||||||
|
description: "Advanced features for power users",
|
||||||
|
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||||
|
passwordManager: {
|
||||||
|
type: "standalone",
|
||||||
|
annualPrice: undefined as any, // self-host will have these prices empty
|
||||||
|
annualPricePerAdditionalStorageGB: undefined as any,
|
||||||
|
providedStorageGB: undefined as any,
|
||||||
|
features: [
|
||||||
|
{ key: "feature1", value: "Feature 1" },
|
||||||
|
{ key: "feature2", value: "Feature 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
|
||||||
|
of([selfHostedPremiumTier]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const selfHostedFixture = TestBed.createComponent(PremiumUpgradeDialogComponent);
|
||||||
|
const selfHostedComponent = selfHostedFixture.componentInstance;
|
||||||
|
selfHostedFixture.detectChanges();
|
||||||
|
|
||||||
|
const cardDetails = await firstValueFrom(selfHostedComponent["cardDetails$"]);
|
||||||
|
|
||||||
|
expect(cardDetails?.title).toBe("Premium");
|
||||||
|
expect(cardDetails?.price).toBeUndefined();
|
||||||
|
expect(cardDetails?.features).toEqual(["Feature 1", "Feature 2"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,23 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockPremiumTierNoPricingData: PersonalSubscriptionPricingTier = {
|
||||||
|
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||||
|
name: "Premium",
|
||||||
|
description: "Complete online security",
|
||||||
|
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||||
|
passwordManager: {
|
||||||
|
type: "standalone",
|
||||||
|
features: [
|
||||||
|
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
||||||
|
{ key: "secureFileStorage", value: "Secure file storage" },
|
||||||
|
{ key: "emergencyAccess", value: "Emergency access" },
|
||||||
|
{ key: "breachMonitoring", value: "Breach monitoring" },
|
||||||
|
{ key: "andMoreFeatures", value: "And more!" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Billing/Premium Upgrade Dialog",
|
title: "Billing/Premium Upgrade Dialog",
|
||||||
component: PremiumUpgradeDialogComponent,
|
component: PremiumUpgradeDialogComponent,
|
||||||
@@ -86,11 +103,11 @@ export default {
|
|||||||
t: (key: string) => {
|
t: (key: string) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "upgradeNow":
|
case "upgradeNow":
|
||||||
return "Upgrade Now";
|
return "Upgrade now";
|
||||||
case "month":
|
case "month":
|
||||||
return "month";
|
return "month";
|
||||||
case "upgradeToPremium":
|
case "upgradeToPremium":
|
||||||
return "Upgrade To Premium";
|
return "Upgrade to Premium";
|
||||||
default:
|
default:
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@@ -116,3 +133,18 @@ export default {
|
|||||||
|
|
||||||
type Story = StoryObj<PremiumUpgradeDialogComponent>;
|
type Story = StoryObj<PremiumUpgradeDialogComponent>;
|
||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const NoPricingData: Story = {
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SubscriptionPricingServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getPersonalSubscriptionPricingTiers$: () => of([mockPremiumTierNoPricingData]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||||
import { catchError, EMPTY, firstValueFrom, map, Observable } from "rxjs";
|
import { catchError, EMPTY, firstValueFrom, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { SubscriptionPricingCardDetails } from "@bitwarden/angular/billing/types/subscription-pricing-card-details";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
||||||
import {
|
import {
|
||||||
PersonalSubscriptionPricingTier,
|
PersonalSubscriptionPricingTier,
|
||||||
PersonalSubscriptionPricingTierIds,
|
PersonalSubscriptionPricingTierIds,
|
||||||
SubscriptionCadence,
|
|
||||||
SubscriptionCadenceIds,
|
SubscriptionCadenceIds,
|
||||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
} 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";
|
||||||
@@ -16,7 +16,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import {
|
import {
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
ButtonType,
|
|
||||||
CenterPositionStrategy,
|
CenterPositionStrategy,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
DialogRef,
|
DialogRef,
|
||||||
@@ -27,14 +26,6 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { LogService } from "@bitwarden/logging";
|
import { LogService } from "@bitwarden/logging";
|
||||||
|
|
||||||
type CardDetails = {
|
|
||||||
title: string;
|
|
||||||
tagline: string;
|
|
||||||
price: { amount: number; cadence: SubscriptionCadence };
|
|
||||||
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
|
|
||||||
features: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "billing-premium-upgrade-dialog",
|
selector: "billing-premium-upgrade-dialog",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -51,9 +42,8 @@ type CardDetails = {
|
|||||||
templateUrl: "./premium-upgrade-dialog.component.html",
|
templateUrl: "./premium-upgrade-dialog.component.html",
|
||||||
})
|
})
|
||||||
export class PremiumUpgradeDialogComponent {
|
export class PremiumUpgradeDialogComponent {
|
||||||
protected cardDetails$: Observable<CardDetails | null> = this.subscriptionPricingService
|
protected cardDetails$: Observable<SubscriptionPricingCardDetails | null> =
|
||||||
.getPersonalSubscriptionPricingTiers$()
|
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe(
|
||||||
.pipe(
|
|
||||||
map((tiers) => tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium)),
|
map((tiers) => tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium)),
|
||||||
map((tier) => this.mapPremiumTierToCardDetails(tier!)),
|
map((tier) => this.mapPremiumTierToCardDetails(tier!)),
|
||||||
catchError((error: unknown) => {
|
catchError((error: unknown) => {
|
||||||
@@ -91,14 +81,18 @@ export class PremiumUpgradeDialogComponent {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapPremiumTierToCardDetails(tier: PersonalSubscriptionPricingTier): CardDetails {
|
private mapPremiumTierToCardDetails(
|
||||||
|
tier: PersonalSubscriptionPricingTier,
|
||||||
|
): SubscriptionPricingCardDetails {
|
||||||
return {
|
return {
|
||||||
title: tier.name,
|
title: tier.name,
|
||||||
tagline: tier.description,
|
tagline: tier.description,
|
||||||
price: {
|
price: tier.passwordManager.annualPrice
|
||||||
|
? {
|
||||||
amount: tier.passwordManager.annualPrice / 12,
|
amount: tier.passwordManager.annualPrice / 12,
|
||||||
cadence: SubscriptionCadenceIds.Monthly,
|
cadence: SubscriptionCadenceIds.Monthly,
|
||||||
},
|
}
|
||||||
|
: undefined,
|
||||||
button: {
|
button: {
|
||||||
text: this.i18nService.t("upgradeNow"),
|
text: this.i18nService.t("upgradeNow"),
|
||||||
type: "primary",
|
type: "primary",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||||
|
import { ButtonType } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export type SubscriptionPricingCardDetails = {
|
||||||
|
title: string;
|
||||||
|
tagline: string;
|
||||||
|
price?: { amount: number; cadence: SubscriptionCadence };
|
||||||
|
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
@@ -1498,7 +1498,13 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SubscriptionPricingServiceAbstraction,
|
provide: SubscriptionPricingServiceAbstraction,
|
||||||
useClass: DefaultSubscriptionPricingService,
|
useClass: DefaultSubscriptionPricingService,
|
||||||
deps: [BillingApiServiceAbstraction, ConfigService, I18nServiceAbstraction, LogService],
|
deps: [
|
||||||
|
BillingApiServiceAbstraction,
|
||||||
|
ConfigService,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
LogService,
|
||||||
|
EnvironmentService,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: OrganizationManagementPreferencesService,
|
provide: OrganizationManagementPreferencesService,
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
|||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response";
|
import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import {
|
||||||
|
EnvironmentService,
|
||||||
|
Region,
|
||||||
|
} 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";
|
||||||
import { LogService } from "@bitwarden/logging";
|
import { LogService } from "@bitwarden/logging";
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
let configService: MockProxy<ConfigService>;
|
let configService: MockProxy<ConfigService>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
|
|
||||||
const mockFamiliesPlan = {
|
const mockFamiliesPlan = {
|
||||||
type: PlanType.FamiliesAnnually2025,
|
type: PlanType.FamiliesAnnually2025,
|
||||||
@@ -328,19 +333,32 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setupEnvironmentService = (
|
||||||
|
envService: MockProxy<EnvironmentService>,
|
||||||
|
region: Region = Region.US,
|
||||||
|
) => {
|
||||||
|
envService.environment$ = of({
|
||||||
|
getRegion: () => region,
|
||||||
|
isCloud: () => region !== Region.SelfHosted,
|
||||||
|
} as any);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
billingApiService = mock<BillingApiServiceAbstraction>();
|
billingApiService = mock<BillingApiServiceAbstraction>();
|
||||||
configService = mock<ConfigService>();
|
configService = mock<ConfigService>();
|
||||||
|
environmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
billingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
billingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
||||||
billingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
billingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(false)); // Default to false (use hardcoded value)
|
configService.getFeatureFlag$.mockReturnValue(of(false)); // Default to false (use hardcoded value)
|
||||||
|
setupEnvironmentService(environmentService);
|
||||||
|
|
||||||
service = new DefaultSubscriptionPricingService(
|
service = new DefaultSubscriptionPricingService(
|
||||||
billingApiService,
|
billingApiService,
|
||||||
configService,
|
configService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
|
environmentService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -419,11 +437,13 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
const errorConfigService = mock<ConfigService>();
|
const errorConfigService = mock<ConfigService>();
|
||||||
const errorI18nService = mock<I18nService>();
|
const errorI18nService = mock<I18nService>();
|
||||||
const errorLogService = mock<LogService>();
|
const errorLogService = mock<LogService>();
|
||||||
|
const errorEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
const testError = new Error("API error");
|
const testError = new Error("API error");
|
||||||
errorBillingApiService.getPlans.mockRejectedValue(testError);
|
errorBillingApiService.getPlans.mockRejectedValue(testError);
|
||||||
errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
||||||
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
setupEnvironmentService(errorEnvironmentService);
|
||||||
|
|
||||||
errorI18nService.t.mockImplementation((key: string) => key);
|
errorI18nService.t.mockImplementation((key: string) => key);
|
||||||
|
|
||||||
@@ -432,6 +452,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
errorConfigService,
|
errorConfigService,
|
||||||
errorI18nService,
|
errorI18nService,
|
||||||
errorLogService,
|
errorLogService,
|
||||||
|
errorEnvironmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
errorService.getPersonalSubscriptionPricingTiers$().subscribe({
|
errorService.getPersonalSubscriptionPricingTiers$().subscribe({
|
||||||
@@ -605,11 +626,13 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
const errorConfigService = mock<ConfigService>();
|
const errorConfigService = mock<ConfigService>();
|
||||||
const errorI18nService = mock<I18nService>();
|
const errorI18nService = mock<I18nService>();
|
||||||
const errorLogService = mock<LogService>();
|
const errorLogService = mock<LogService>();
|
||||||
|
const errorEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
const testError = new Error("API error");
|
const testError = new Error("API error");
|
||||||
errorBillingApiService.getPlans.mockRejectedValue(testError);
|
errorBillingApiService.getPlans.mockRejectedValue(testError);
|
||||||
errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
||||||
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
setupEnvironmentService(errorEnvironmentService);
|
||||||
|
|
||||||
errorI18nService.t.mockImplementation((key: string) => key);
|
errorI18nService.t.mockImplementation((key: string) => key);
|
||||||
|
|
||||||
@@ -618,6 +641,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
errorConfigService,
|
errorConfigService,
|
||||||
errorI18nService,
|
errorI18nService,
|
||||||
errorLogService,
|
errorLogService,
|
||||||
|
errorEnvironmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
errorService.getBusinessSubscriptionPricingTiers$().subscribe({
|
errorService.getBusinessSubscriptionPricingTiers$().subscribe({
|
||||||
@@ -848,11 +872,13 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
const errorConfigService = mock<ConfigService>();
|
const errorConfigService = mock<ConfigService>();
|
||||||
const errorI18nService = mock<I18nService>();
|
const errorI18nService = mock<I18nService>();
|
||||||
const errorLogService = mock<LogService>();
|
const errorLogService = mock<LogService>();
|
||||||
|
const errorEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
const testError = new Error("API error");
|
const testError = new Error("API error");
|
||||||
errorBillingApiService.getPlans.mockRejectedValue(testError);
|
errorBillingApiService.getPlans.mockRejectedValue(testError);
|
||||||
errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
||||||
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
setupEnvironmentService(errorEnvironmentService);
|
||||||
|
|
||||||
errorI18nService.t.mockImplementation((key: string) => key);
|
errorI18nService.t.mockImplementation((key: string) => key);
|
||||||
|
|
||||||
@@ -861,6 +887,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
errorConfigService,
|
errorConfigService,
|
||||||
errorI18nService,
|
errorI18nService,
|
||||||
errorLogService,
|
errorLogService,
|
||||||
|
errorEnvironmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
errorService.getDeveloperSubscriptionPricingTiers$().subscribe({
|
errorService.getDeveloperSubscriptionPricingTiers$().subscribe({
|
||||||
@@ -883,17 +910,20 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
it("should handle getPremiumPlan() error when getPlans() succeeds", (done) => {
|
it("should handle getPremiumPlan() error when getPlans() succeeds", (done) => {
|
||||||
const errorBillingApiService = mock<BillingApiServiceAbstraction>();
|
const errorBillingApiService = mock<BillingApiServiceAbstraction>();
|
||||||
const errorConfigService = mock<ConfigService>();
|
const errorConfigService = mock<ConfigService>();
|
||||||
|
const errorEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
const testError = new Error("Premium plan API error");
|
const testError = new Error("Premium plan API error");
|
||||||
errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
||||||
errorBillingApiService.getPremiumPlan.mockRejectedValue(testError);
|
errorBillingApiService.getPremiumPlan.mockRejectedValue(testError);
|
||||||
errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag to use premium plan API
|
errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag to use premium plan API
|
||||||
|
setupEnvironmentService(errorEnvironmentService);
|
||||||
|
|
||||||
const errorService = new DefaultSubscriptionPricingService(
|
const errorService = new DefaultSubscriptionPricingService(
|
||||||
errorBillingApiService,
|
errorBillingApiService,
|
||||||
errorConfigService,
|
errorConfigService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
|
errorEnvironmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
errorService.getPersonalSubscriptionPricingTiers$().subscribe({
|
errorService.getPersonalSubscriptionPricingTiers$().subscribe({
|
||||||
@@ -914,88 +944,6 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle malformed premium plan API response", (done) => {
|
|
||||||
const errorBillingApiService = mock<BillingApiServiceAbstraction>();
|
|
||||||
const errorConfigService = mock<ConfigService>();
|
|
||||||
const testError = new TypeError("Cannot read properties of undefined (reading 'price')");
|
|
||||||
|
|
||||||
// Malformed response missing the Seat property
|
|
||||||
const malformedResponse = {
|
|
||||||
Storage: {
|
|
||||||
StripePriceId: "price_storage",
|
|
||||||
Price: 4,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
|
||||||
errorBillingApiService.getPremiumPlan.mockResolvedValue(malformedResponse as any);
|
|
||||||
errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag
|
|
||||||
|
|
||||||
const errorService = new DefaultSubscriptionPricingService(
|
|
||||||
errorBillingApiService,
|
|
||||||
errorConfigService,
|
|
||||||
i18nService,
|
|
||||||
logService,
|
|
||||||
);
|
|
||||||
|
|
||||||
errorService.getPersonalSubscriptionPricingTiers$().subscribe({
|
|
||||||
next: () => {
|
|
||||||
fail("Observable should error, not return a value");
|
|
||||||
},
|
|
||||||
error: (error: unknown) => {
|
|
||||||
expect(logService.error).toHaveBeenCalledWith(
|
|
||||||
"Failed to load personal subscription pricing tiers",
|
|
||||||
testError,
|
|
||||||
);
|
|
||||||
expect(error).toEqual(testError);
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle malformed premium plan with invalid price types", (done) => {
|
|
||||||
const errorBillingApiService = mock<BillingApiServiceAbstraction>();
|
|
||||||
const errorConfigService = mock<ConfigService>();
|
|
||||||
const testError = new TypeError("Cannot read properties of undefined (reading 'price')");
|
|
||||||
|
|
||||||
// Malformed response with price as string instead of number
|
|
||||||
const malformedResponse = {
|
|
||||||
Seat: {
|
|
||||||
StripePriceId: "price_seat",
|
|
||||||
Price: "10", // Should be a number
|
|
||||||
},
|
|
||||||
Storage: {
|
|
||||||
StripePriceId: "price_storage",
|
|
||||||
Price: 4,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
|
||||||
errorBillingApiService.getPremiumPlan.mockResolvedValue(malformedResponse as any);
|
|
||||||
errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag
|
|
||||||
|
|
||||||
const errorService = new DefaultSubscriptionPricingService(
|
|
||||||
errorBillingApiService,
|
|
||||||
errorConfigService,
|
|
||||||
i18nService,
|
|
||||||
logService,
|
|
||||||
);
|
|
||||||
|
|
||||||
errorService.getPersonalSubscriptionPricingTiers$().subscribe({
|
|
||||||
next: () => {
|
|
||||||
fail("Observable should error, not return a value");
|
|
||||||
},
|
|
||||||
error: (error: unknown) => {
|
|
||||||
expect(logService.error).toHaveBeenCalledWith(
|
|
||||||
"Failed to load personal subscription pricing tiers",
|
|
||||||
testError,
|
|
||||||
);
|
|
||||||
expect(error).toEqual(testError);
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Observable behavior and caching", () => {
|
describe("Observable behavior and caching", () => {
|
||||||
@@ -1015,10 +963,12 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
// Create a new mock to avoid conflicts with beforeEach setup
|
// Create a new mock to avoid conflicts with beforeEach setup
|
||||||
const newBillingApiService = mock<BillingApiServiceAbstraction>();
|
const newBillingApiService = mock<BillingApiServiceAbstraction>();
|
||||||
const newConfigService = mock<ConfigService>();
|
const newConfigService = mock<ConfigService>();
|
||||||
|
const newEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
||||||
newBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
newBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
|
||||||
newConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
newConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
setupEnvironmentService(newEnvironmentService);
|
||||||
|
|
||||||
const getPremiumPlanSpy = jest.spyOn(newBillingApiService, "getPremiumPlan");
|
const getPremiumPlanSpy = jest.spyOn(newBillingApiService, "getPremiumPlan");
|
||||||
|
|
||||||
@@ -1028,6 +978,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
newConfigService,
|
newConfigService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
|
newEnvironmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Subscribe to the premium pricing tier multiple times
|
// Subscribe to the premium pricing tier multiple times
|
||||||
@@ -1042,6 +993,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
// Create a new mock to test from scratch
|
// Create a new mock to test from scratch
|
||||||
const newBillingApiService = mock<BillingApiServiceAbstraction>();
|
const newBillingApiService = mock<BillingApiServiceAbstraction>();
|
||||||
const newConfigService = mock<ConfigService>();
|
const newConfigService = mock<ConfigService>();
|
||||||
|
const newEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
|
||||||
newBillingApiService.getPremiumPlan.mockResolvedValue({
|
newBillingApiService.getPremiumPlan.mockResolvedValue({
|
||||||
@@ -1049,6 +1001,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
storage: { price: 999 },
|
storage: { price: 999 },
|
||||||
} as PremiumPlanResponse);
|
} as PremiumPlanResponse);
|
||||||
newConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
newConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
setupEnvironmentService(newEnvironmentService);
|
||||||
|
|
||||||
// Create a new service instance with the feature flag disabled
|
// Create a new service instance with the feature flag disabled
|
||||||
const newService = new DefaultSubscriptionPricingService(
|
const newService = new DefaultSubscriptionPricingService(
|
||||||
@@ -1056,6 +1009,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
newConfigService,
|
newConfigService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
|
newEnvironmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Subscribe with feature flag disabled
|
// Subscribe with feature flag disabled
|
||||||
@@ -1071,4 +1025,66 @@ describe("DefaultSubscriptionPricingService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Self-hosted environment behavior", () => {
|
||||||
|
it("should not call API for self-hosted environment", () => {
|
||||||
|
const selfHostedBillingApiService = mock<BillingApiServiceAbstraction>();
|
||||||
|
const selfHostedConfigService = mock<ConfigService>();
|
||||||
|
const selfHostedEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
|
const getPlansSpy = jest.spyOn(selfHostedBillingApiService, "getPlans");
|
||||||
|
const getPremiumPlanSpy = jest.spyOn(selfHostedBillingApiService, "getPremiumPlan");
|
||||||
|
|
||||||
|
selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted);
|
||||||
|
|
||||||
|
const selfHostedService = new DefaultSubscriptionPricingService(
|
||||||
|
selfHostedBillingApiService,
|
||||||
|
selfHostedConfigService,
|
||||||
|
i18nService,
|
||||||
|
logService,
|
||||||
|
selfHostedEnvironmentService,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger subscriptions by calling the methods
|
||||||
|
selfHostedService.getPersonalSubscriptionPricingTiers$().subscribe();
|
||||||
|
selfHostedService.getBusinessSubscriptionPricingTiers$().subscribe();
|
||||||
|
selfHostedService.getDeveloperSubscriptionPricingTiers$().subscribe();
|
||||||
|
|
||||||
|
// API should not be called for self-hosted environments
|
||||||
|
expect(getPlansSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getPremiumPlanSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return valid tier structure with undefined prices for self-hosted", (done) => {
|
||||||
|
const selfHostedBillingApiService = mock<BillingApiServiceAbstraction>();
|
||||||
|
const selfHostedConfigService = mock<ConfigService>();
|
||||||
|
const selfHostedEnvironmentService = mock<EnvironmentService>();
|
||||||
|
|
||||||
|
selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted);
|
||||||
|
|
||||||
|
const selfHostedService = new DefaultSubscriptionPricingService(
|
||||||
|
selfHostedBillingApiService,
|
||||||
|
selfHostedConfigService,
|
||||||
|
i18nService,
|
||||||
|
logService,
|
||||||
|
selfHostedEnvironmentService,
|
||||||
|
);
|
||||||
|
|
||||||
|
selfHostedService.getPersonalSubscriptionPricingTiers$().subscribe((tiers) => {
|
||||||
|
expect(tiers).toHaveLength(2); // Premium and Families
|
||||||
|
|
||||||
|
const premiumTier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Premium);
|
||||||
|
expect(premiumTier).toBeDefined();
|
||||||
|
expect(premiumTier?.passwordManager.annualPrice).toBeUndefined();
|
||||||
|
expect(premiumTier?.passwordManager.annualPricePerAdditionalStorageGB).toBeUndefined();
|
||||||
|
expect(premiumTier?.passwordManager.providedStorageGB).toBeUndefined();
|
||||||
|
expect(premiumTier?.passwordManager.features).toBeDefined();
|
||||||
|
expect(premiumTier?.passwordManager.features.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/p
|
|||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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";
|
||||||
import { LogService } from "@bitwarden/logging";
|
import { LogService } from "@bitwarden/logging";
|
||||||
|
|
||||||
@@ -47,11 +48,13 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets personal subscription pricing tiers (Premium and Families).
|
* Gets personal subscription pricing tiers (Premium and Families).
|
||||||
* Throws any errors that occur during api request so callers must handle errors.
|
* Throws any errors that occur during api request so callers must handle errors.
|
||||||
|
* Pricing information will be undefined if current environment is self-hosted.
|
||||||
* @returns An observable of an array of personal subscription pricing tiers.
|
* @returns An observable of an array of personal subscription pricing tiers.
|
||||||
* @throws Error if any errors occur during api request.
|
* @throws Error if any errors occur during api request.
|
||||||
*/
|
*/
|
||||||
@@ -66,6 +69,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
/**
|
/**
|
||||||
* Gets business subscription pricing tiers (Teams, Enterprise, and Custom).
|
* Gets business subscription pricing tiers (Teams, Enterprise, and Custom).
|
||||||
* Throws any errors that occur during api request so callers must handle errors.
|
* Throws any errors that occur during api request so callers must handle errors.
|
||||||
|
* Pricing information will be undefined if current environment is self-hosted.
|
||||||
* @returns An observable of an array of business subscription pricing tiers.
|
* @returns An observable of an array of business subscription pricing tiers.
|
||||||
* @throws Error if any errors occur during api request.
|
* @throws Error if any errors occur during api request.
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +84,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
/**
|
/**
|
||||||
* Gets developer subscription pricing tiers (Free, Teams, and Enterprise).
|
* Gets developer subscription pricing tiers (Free, Teams, and Enterprise).
|
||||||
* Throws any errors that occur during api request so callers must handle errors.
|
* Throws any errors that occur during api request so callers must handle errors.
|
||||||
|
* Pricing information will be undefined if current environment is self-hosted.
|
||||||
* @returns An observable of an array of business subscription pricing tiers for developers.
|
* @returns An observable of an array of business subscription pricing tiers for developers.
|
||||||
* @throws Error if any errors occur during api request.
|
* @throws Error if any errors occur during api request.
|
||||||
*/
|
*/
|
||||||
@@ -91,17 +96,30 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
private plansResponse$: Observable<ListResponse<PlanResponse>> = from(
|
private organizationPlansResponse$: Observable<ListResponse<PlanResponse>> =
|
||||||
this.billingApiService.getPlans(),
|
this.environmentService.environment$.pipe(
|
||||||
).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
take(1),
|
||||||
|
switchMap((environment) =>
|
||||||
|
!environment.isCloud()
|
||||||
|
? of({ data: [] } as unknown as ListResponse<PlanResponse>)
|
||||||
|
: from(this.billingApiService.getPlans()),
|
||||||
|
),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: false }),
|
||||||
|
);
|
||||||
|
|
||||||
private premiumPlanResponse$: Observable<PremiumPlanResponse> = from(
|
private premiumPlanResponse$: Observable<PremiumPlanResponse> =
|
||||||
this.billingApiService.getPremiumPlan(),
|
this.environmentService.environment$.pipe(
|
||||||
).pipe(
|
take(1),
|
||||||
|
switchMap((environment) =>
|
||||||
|
!environment.isCloud()
|
||||||
|
? of({ seat: undefined, storage: undefined } as unknown as PremiumPlanResponse)
|
||||||
|
: from(this.billingApiService.getPremiumPlan()).pipe(
|
||||||
catchError((error: unknown) => {
|
catchError((error: unknown) => {
|
||||||
this.logService.error("Failed to fetch premium plan from API", error);
|
this.logService.error("Failed to fetch premium plan from API", error);
|
||||||
return throwError(() => error); // Re-throw to propagate to higher-level error handler
|
return throwError(() => error); // Re-throw to propagate to higher-level error handler
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
shareReplay({ bufferSize: 1, refCount: false }),
|
shareReplay({ bufferSize: 1, refCount: false }),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -113,9 +131,9 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
fetchPremiumFromPricingService
|
fetchPremiumFromPricingService
|
||||||
? this.premiumPlanResponse$.pipe(
|
? this.premiumPlanResponse$.pipe(
|
||||||
map((premiumPlan) => ({
|
map((premiumPlan) => ({
|
||||||
seat: premiumPlan.seat.price,
|
seat: premiumPlan.seat?.price,
|
||||||
storage: premiumPlan.storage.price,
|
storage: premiumPlan.storage?.price,
|
||||||
provided: premiumPlan.storage.provided,
|
provided: premiumPlan.storage?.provided,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
: of({
|
: of({
|
||||||
@@ -145,14 +163,15 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
private families$: Observable<PersonalSubscriptionPricingTier> = this.plansResponse$.pipe(
|
private families$: Observable<PersonalSubscriptionPricingTier> =
|
||||||
|
this.organizationPlansResponse$.pipe(
|
||||||
combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.PM26462_Milestone_3)),
|
combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.PM26462_Milestone_3)),
|
||||||
map(([plans, milestone3FeatureEnabled]) => {
|
map(([plans, milestone3FeatureEnabled]) => {
|
||||||
const familiesPlan = plans.data.find(
|
const familiesPlan = plans.data.find(
|
||||||
(plan) =>
|
(plan) =>
|
||||||
plan.type ===
|
plan.type ===
|
||||||
(milestone3FeatureEnabled ? PlanType.FamiliesAnnually : PlanType.FamiliesAnnually2025),
|
(milestone3FeatureEnabled ? PlanType.FamiliesAnnually : PlanType.FamiliesAnnually2025),
|
||||||
)!;
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: PersonalSubscriptionPricingTierIds.Families,
|
id: PersonalSubscriptionPricingTierIds.Families,
|
||||||
@@ -161,11 +180,11 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
availableCadences: [SubscriptionCadenceIds.Annually],
|
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||||
passwordManager: {
|
passwordManager: {
|
||||||
type: "packaged",
|
type: "packaged",
|
||||||
users: familiesPlan.PasswordManager.baseSeats,
|
users: familiesPlan?.PasswordManager?.baseSeats,
|
||||||
annualPrice: familiesPlan.PasswordManager.basePrice,
|
annualPrice: familiesPlan?.PasswordManager?.basePrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
familiesPlan.PasswordManager.additionalStoragePricePerGb,
|
familiesPlan?.PasswordManager?.additionalStoragePricePerGb,
|
||||||
providedStorageGB: familiesPlan.PasswordManager.baseStorageGb,
|
providedStorageGB: familiesPlan?.PasswordManager?.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.premiumAccounts(),
|
this.featureTranslations.premiumAccounts(),
|
||||||
this.featureTranslations.familiesUnlimitedSharing(),
|
this.featureTranslations.familiesUnlimitedSharing(),
|
||||||
@@ -177,9 +196,9 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
private free$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
|
private free$: Observable<BusinessSubscriptionPricingTier> = this.organizationPlansResponse$.pipe(
|
||||||
map((plans): BusinessSubscriptionPricingTier => {
|
map((plans): BusinessSubscriptionPricingTier => {
|
||||||
const freePlan = plans.data.find((plan) => plan.type === PlanType.Free)!;
|
const freePlan = plans.data.find((plan) => plan.type === PlanType.Free);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: BusinessSubscriptionPricingTierIds.Free,
|
id: BusinessSubscriptionPricingTierIds.Free,
|
||||||
@@ -189,8 +208,10 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
passwordManager: {
|
passwordManager: {
|
||||||
type: "free",
|
type: "free",
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.limitedUsersV2(freePlan.PasswordManager.maxSeats),
|
this.featureTranslations.limitedUsersV2(freePlan?.PasswordManager?.maxSeats),
|
||||||
this.featureTranslations.limitedCollectionsV2(freePlan.PasswordManager.maxCollections),
|
this.featureTranslations.limitedCollectionsV2(
|
||||||
|
freePlan?.PasswordManager?.maxCollections,
|
||||||
|
),
|
||||||
this.featureTranslations.alwaysFree(),
|
this.featureTranslations.alwaysFree(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -198,16 +219,17 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
type: "free",
|
type: "free",
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.twoSecretsIncluded(),
|
this.featureTranslations.twoSecretsIncluded(),
|
||||||
this.featureTranslations.projectsIncludedV2(freePlan.SecretsManager.maxProjects),
|
this.featureTranslations.projectsIncludedV2(freePlan?.SecretsManager?.maxProjects),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
private teams$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
|
private teams$: Observable<BusinessSubscriptionPricingTier> =
|
||||||
|
this.organizationPlansResponse$.pipe(
|
||||||
map((plans) => {
|
map((plans) => {
|
||||||
const annualTeamsPlan = plans.data.find((plan) => plan.type === PlanType.TeamsAnnually)!;
|
const annualTeamsPlan = plans.data.find((plan) => plan.type === PlanType.TeamsAnnually);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: BusinessSubscriptionPricingTierIds.Teams,
|
id: BusinessSubscriptionPricingTierIds.Teams,
|
||||||
@@ -216,10 +238,10 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
availableCadences: [SubscriptionCadenceIds.Annually, SubscriptionCadenceIds.Monthly],
|
availableCadences: [SubscriptionCadenceIds.Annually, SubscriptionCadenceIds.Monthly],
|
||||||
passwordManager: {
|
passwordManager: {
|
||||||
type: "scalable",
|
type: "scalable",
|
||||||
annualPricePerUser: annualTeamsPlan.PasswordManager.seatPrice,
|
annualPricePerUser: annualTeamsPlan?.PasswordManager?.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
annualTeamsPlan.PasswordManager.additionalStoragePricePerGb,
|
annualTeamsPlan?.PasswordManager?.additionalStoragePricePerGb,
|
||||||
providedStorageGB: annualTeamsPlan.PasswordManager.baseStorageGb,
|
providedStorageGB: annualTeamsPlan?.PasswordManager?.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.secureItemSharing(),
|
this.featureTranslations.secureItemSharing(),
|
||||||
this.featureTranslations.eventLogMonitoring(),
|
this.featureTranslations.eventLogMonitoring(),
|
||||||
@@ -229,13 +251,13 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
},
|
},
|
||||||
secretsManager: {
|
secretsManager: {
|
||||||
type: "scalable",
|
type: "scalable",
|
||||||
annualPricePerUser: annualTeamsPlan.SecretsManager.seatPrice,
|
annualPricePerUser: annualTeamsPlan?.SecretsManager?.seatPrice,
|
||||||
annualPricePerAdditionalServiceAccount:
|
annualPricePerAdditionalServiceAccount:
|
||||||
annualTeamsPlan.SecretsManager.additionalPricePerServiceAccount,
|
annualTeamsPlan?.SecretsManager?.additionalPricePerServiceAccount,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.unlimitedSecretsAndProjects(),
|
this.featureTranslations.unlimitedSecretsAndProjects(),
|
||||||
this.featureTranslations.includedMachineAccountsV2(
|
this.featureTranslations.includedMachineAccountsV2(
|
||||||
annualTeamsPlan.SecretsManager.baseServiceAccount,
|
annualTeamsPlan?.SecretsManager?.baseServiceAccount,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -243,11 +265,12 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
private enterprise$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
|
private enterprise$: Observable<BusinessSubscriptionPricingTier> =
|
||||||
|
this.organizationPlansResponse$.pipe(
|
||||||
map((plans) => {
|
map((plans) => {
|
||||||
const annualEnterprisePlan = plans.data.find(
|
const annualEnterprisePlan = plans.data.find(
|
||||||
(plan) => plan.type === PlanType.EnterpriseAnnually,
|
(plan) => plan.type === PlanType.EnterpriseAnnually,
|
||||||
)!;
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: BusinessSubscriptionPricingTierIds.Enterprise,
|
id: BusinessSubscriptionPricingTierIds.Enterprise,
|
||||||
@@ -256,10 +279,10 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
availableCadences: [SubscriptionCadenceIds.Annually, SubscriptionCadenceIds.Monthly],
|
availableCadences: [SubscriptionCadenceIds.Annually, SubscriptionCadenceIds.Monthly],
|
||||||
passwordManager: {
|
passwordManager: {
|
||||||
type: "scalable",
|
type: "scalable",
|
||||||
annualPricePerUser: annualEnterprisePlan.PasswordManager.seatPrice,
|
annualPricePerUser: annualEnterprisePlan?.PasswordManager?.seatPrice,
|
||||||
annualPricePerAdditionalStorageGB:
|
annualPricePerAdditionalStorageGB:
|
||||||
annualEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
|
annualEnterprisePlan?.PasswordManager?.additionalStoragePricePerGb,
|
||||||
providedStorageGB: annualEnterprisePlan.PasswordManager.baseStorageGb,
|
providedStorageGB: annualEnterprisePlan?.PasswordManager?.baseStorageGb,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.enterpriseSecurityPolicies(),
|
this.featureTranslations.enterpriseSecurityPolicies(),
|
||||||
this.featureTranslations.passwordLessSso(),
|
this.featureTranslations.passwordLessSso(),
|
||||||
@@ -270,13 +293,13 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
},
|
},
|
||||||
secretsManager: {
|
secretsManager: {
|
||||||
type: "scalable",
|
type: "scalable",
|
||||||
annualPricePerUser: annualEnterprisePlan.SecretsManager.seatPrice,
|
annualPricePerUser: annualEnterprisePlan?.SecretsManager?.seatPrice,
|
||||||
annualPricePerAdditionalServiceAccount:
|
annualPricePerAdditionalServiceAccount:
|
||||||
annualEnterprisePlan.SecretsManager.additionalPricePerServiceAccount,
|
annualEnterprisePlan?.SecretsManager?.additionalPricePerServiceAccount,
|
||||||
features: [
|
features: [
|
||||||
this.featureTranslations.unlimitedUsers(),
|
this.featureTranslations.unlimitedUsers(),
|
||||||
this.featureTranslations.includedMachineAccountsV2(
|
this.featureTranslations.includedMachineAccountsV2(
|
||||||
annualEnterprisePlan.SecretsManager.baseServiceAccount,
|
annualEnterprisePlan?.SecretsManager?.baseServiceAccount,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -284,7 +307,8 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
private custom$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
|
private custom$: Observable<BusinessSubscriptionPricingTier> =
|
||||||
|
this.organizationPlansResponse$.pipe(
|
||||||
map(
|
map(
|
||||||
(): BusinessSubscriptionPricingTier => ({
|
(): BusinessSubscriptionPricingTier => ({
|
||||||
id: BusinessSubscriptionPricingTierIds.Custom,
|
id: BusinessSubscriptionPricingTierIds.Custom,
|
||||||
@@ -340,11 +364,11 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
key: "familiesSharedStorage",
|
key: "familiesSharedStorage",
|
||||||
value: this.i18nService.t("familiesSharedStorage"),
|
value: this.i18nService.t("familiesSharedStorage"),
|
||||||
}),
|
}),
|
||||||
limitedUsersV2: (users: number) => ({
|
limitedUsersV2: (users?: number) => ({
|
||||||
key: "limitedUsersV2",
|
key: "limitedUsersV2",
|
||||||
value: this.i18nService.t("limitedUsersV2", users),
|
value: this.i18nService.t("limitedUsersV2", users),
|
||||||
}),
|
}),
|
||||||
limitedCollectionsV2: (collections: number) => ({
|
limitedCollectionsV2: (collections?: number) => ({
|
||||||
key: "limitedCollectionsV2",
|
key: "limitedCollectionsV2",
|
||||||
value: this.i18nService.t("limitedCollectionsV2", collections),
|
value: this.i18nService.t("limitedCollectionsV2", collections),
|
||||||
}),
|
}),
|
||||||
@@ -356,7 +380,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
key: "twoSecretsIncluded",
|
key: "twoSecretsIncluded",
|
||||||
value: this.i18nService.t("twoSecretsIncluded"),
|
value: this.i18nService.t("twoSecretsIncluded"),
|
||||||
}),
|
}),
|
||||||
projectsIncludedV2: (projects: number) => ({
|
projectsIncludedV2: (projects?: number) => ({
|
||||||
key: "projectsIncludedV2",
|
key: "projectsIncludedV2",
|
||||||
value: this.i18nService.t("projectsIncludedV2", projects),
|
value: this.i18nService.t("projectsIncludedV2", projects),
|
||||||
}),
|
}),
|
||||||
@@ -380,7 +404,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
|||||||
key: "unlimitedSecretsAndProjects",
|
key: "unlimitedSecretsAndProjects",
|
||||||
value: this.i18nService.t("unlimitedSecretsAndProjects"),
|
value: this.i18nService.t("unlimitedSecretsAndProjects"),
|
||||||
}),
|
}),
|
||||||
includedMachineAccountsV2: (included: number) => ({
|
includedMachineAccountsV2: (included?: number) => ({
|
||||||
key: "includedMachineAccountsV2",
|
key: "includedMachineAccountsV2",
|
||||||
value: this.i18nService.t("includedMachineAccountsV2", included),
|
value: this.i18nService.t("includedMachineAccountsV2", included),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -27,26 +27,26 @@ type HasFeatures = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type HasAdditionalStorage = {
|
type HasAdditionalStorage = {
|
||||||
annualPricePerAdditionalStorageGB: number;
|
annualPricePerAdditionalStorageGB?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HasProvidedStorage = {
|
type HasProvidedStorage = {
|
||||||
providedStorageGB: number;
|
providedStorageGB?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StandalonePasswordManager = HasFeatures &
|
type StandalonePasswordManager = HasFeatures &
|
||||||
HasAdditionalStorage &
|
HasAdditionalStorage &
|
||||||
HasProvidedStorage & {
|
HasProvidedStorage & {
|
||||||
type: "standalone";
|
type: "standalone";
|
||||||
annualPrice: number;
|
annualPrice?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PackagedPasswordManager = HasFeatures &
|
type PackagedPasswordManager = HasFeatures &
|
||||||
HasProvidedStorage &
|
HasProvidedStorage &
|
||||||
HasAdditionalStorage & {
|
HasAdditionalStorage & {
|
||||||
type: "packaged";
|
type: "packaged";
|
||||||
users: number;
|
users?: number;
|
||||||
annualPrice: number;
|
annualPrice?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FreePasswordManager = HasFeatures & {
|
type FreePasswordManager = HasFeatures & {
|
||||||
@@ -61,7 +61,7 @@ type ScalablePasswordManager = HasFeatures &
|
|||||||
HasProvidedStorage &
|
HasProvidedStorage &
|
||||||
HasAdditionalStorage & {
|
HasAdditionalStorage & {
|
||||||
type: "scalable";
|
type: "scalable";
|
||||||
annualPricePerUser: number;
|
annualPricePerUser?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FreeSecretsManager = HasFeatures & {
|
type FreeSecretsManager = HasFeatures & {
|
||||||
@@ -70,8 +70,8 @@ type FreeSecretsManager = HasFeatures & {
|
|||||||
|
|
||||||
type ScalableSecretsManager = HasFeatures & {
|
type ScalableSecretsManager = HasFeatures & {
|
||||||
type: "scalable";
|
type: "scalable";
|
||||||
annualPricePerUser: number;
|
annualPricePerUser?: number;
|
||||||
annualPricePerAdditionalServiceAccount: number;
|
annualPricePerAdditionalServiceAccount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PersonalSubscriptionPricingTier = {
|
export type PersonalSubscriptionPricingTier = {
|
||||||
|
|||||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -121,12 +121,12 @@
|
|||||||
"@webcomponents/custom-elements": "1.6.0",
|
"@webcomponents/custom-elements": "1.6.0",
|
||||||
"@yao-pkg/pkg": "6.5.1",
|
"@yao-pkg/pkg": "6.5.1",
|
||||||
"angular-eslint": "20.7.0",
|
"angular-eslint": "20.7.0",
|
||||||
"autoprefixer": "10.4.21",
|
"autoprefixer": "10.4.22",
|
||||||
"axe-playwright": "2.2.2",
|
"axe-playwright": "2.2.2",
|
||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"base64-loader": "1.0.0",
|
"base64-loader": "1.0.0",
|
||||||
"browserslist": "4.28.1",
|
"browserslist": "4.28.1",
|
||||||
"chromatic": "13.3.1",
|
"chromatic": "13.3.4",
|
||||||
"concurrently": "9.2.0",
|
"concurrently": "9.2.0",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
"sass-loader": "16.0.6",
|
"sass-loader": "16.0.6",
|
||||||
"storybook": "9.1.16",
|
"storybook": "9.1.16",
|
||||||
"style-loader": "4.0.0",
|
"style-loader": "4.0.0",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.18",
|
||||||
"ts-jest": "29.4.5",
|
"ts-jest": "29.4.5",
|
||||||
"ts-loader": "9.5.4",
|
"ts-loader": "9.5.4",
|
||||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||||
@@ -1042,6 +1042,44 @@
|
|||||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": {
|
||||||
|
"version": "10.4.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||||
|
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"browserslist": "^4.24.4",
|
||||||
|
"caniuse-lite": "^1.0.30001702",
|
||||||
|
"fraction.js": "^4.3.7",
|
||||||
|
"normalize-range": "^0.1.2",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"postcss-value-parser": "^4.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"autoprefixer": "bin/autoprefixer"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"postcss": "^8.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/babel-loader": {
|
"node_modules/@angular-devkit/build-angular/node_modules/babel-loader": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz",
|
||||||
@@ -16631,9 +16669,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.22",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
||||||
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
"integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -16650,9 +16688,9 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.27.0",
|
||||||
"caniuse-lite": "^1.0.30001702",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
"fraction.js": "^4.3.7",
|
"fraction.js": "^5.3.4",
|
||||||
"normalize-range": "^0.1.2",
|
"normalize-range": "^0.1.2",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
@@ -16667,6 +16705,19 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/autoprefixer/node_modules/fraction.js": {
|
||||||
|
"version": "5.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
|
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/rawify"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -18214,9 +18265,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromatic": {
|
"node_modules/chromatic": {
|
||||||
"version": "13.3.1",
|
"version": "13.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.3.4.tgz",
|
||||||
"integrity": "sha512-qJ/el70Wo7jFgiXPpuukqxCEc7IKiH/e8MjTzIF9uKw+3XZ6GghOTTLC7lGfeZtosiQBMkRlYet77tC4KKHUng==",
|
"integrity": "sha512-TR5rvyH0ESXobBB3bV8jc87AEAFQC7/n+Eb4XWhJz6hW3YNxIQPVjcbgLv+a4oKHEl1dUBueWSoIQsOVGTd+RQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -22947,6 +22998,7 @@
|
|||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
@@ -38234,9 +38286,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
|
||||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -38248,7 +38300,7 @@
|
|||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jiti": "^1.21.6",
|
"jiti": "^1.21.7",
|
||||||
"lilconfig": "^3.1.3",
|
"lilconfig": "^3.1.3",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -38257,7 +38309,7 @@
|
|||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-js": "^4.0.1",
|
"postcss-js": "^4.0.1",
|
||||||
"postcss-load-config": "^4.0.2",
|
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
|
||||||
"postcss-nested": "^6.2.0",
|
"postcss-nested": "^6.2.0",
|
||||||
"postcss-selector-parser": "^6.1.2",
|
"postcss-selector-parser": "^6.1.2",
|
||||||
"resolve": "^1.22.8",
|
"resolve": "^1.22.8",
|
||||||
|
|||||||
@@ -83,12 +83,12 @@
|
|||||||
"@webcomponents/custom-elements": "1.6.0",
|
"@webcomponents/custom-elements": "1.6.0",
|
||||||
"@yao-pkg/pkg": "6.5.1",
|
"@yao-pkg/pkg": "6.5.1",
|
||||||
"angular-eslint": "20.7.0",
|
"angular-eslint": "20.7.0",
|
||||||
"autoprefixer": "10.4.21",
|
"autoprefixer": "10.4.22",
|
||||||
"axe-playwright": "2.2.2",
|
"axe-playwright": "2.2.2",
|
||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"base64-loader": "1.0.0",
|
"base64-loader": "1.0.0",
|
||||||
"browserslist": "4.28.1",
|
"browserslist": "4.28.1",
|
||||||
"chromatic": "13.3.1",
|
"chromatic": "13.3.4",
|
||||||
"concurrently": "9.2.0",
|
"concurrently": "9.2.0",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
"sass-loader": "16.0.6",
|
"sass-loader": "16.0.6",
|
||||||
"storybook": "9.1.16",
|
"storybook": "9.1.16",
|
||||||
"style-loader": "4.0.0",
|
"style-loader": "4.0.0",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.18",
|
||||||
"ts-jest": "29.4.5",
|
"ts-jest": "29.4.5",
|
||||||
"ts-loader": "9.5.4",
|
"ts-loader": "9.5.4",
|
||||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user