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

[PM-24284] - milestone 3 (#17230)

* first draft

# Conflicts:
#	apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts
#	apps/web/src/app/billing/organizations/organization-plans.component.ts
#	libs/common/src/billing/services/subscription-pricing.service.ts
#	libs/common/src/enums/feature-flag.enum.ts

* more filtering for pricing cards

* prettier

* tests

* tests v2
This commit is contained in:
Kyle Denney
2025-11-10 11:50:49 -06:00
committed by GitHub
parent c8281a079b
commit e3acd27dec
13 changed files with 189 additions and 46 deletions

View File

@@ -15,8 +15,9 @@ import { PreValidateSponsorshipResponse } from "@bitwarden/common/admin-console/
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
@@ -43,7 +44,7 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
return;
}
value.plan = PlanType.FamiliesAnnually;
value.plan = this._familyPlan;
value.productTier = ProductTierType.Families;
value.acceptingSponsorship = true;
value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
@@ -63,13 +64,14 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
_selectedFamilyOrganizationId = "";
private _destroy = new Subject<void>();
private _familyPlan: PlanType;
formGroup = this.formBuilder.group({
selectedFamilyOrganizationId: ["", Validators.required],
});
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
private i18nService: I18nService,
private route: ActivatedRoute,
private apiService: ApiService,
@@ -120,6 +122,13 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
this.badToken = !this.preValidateSponsorshipResponse.isTokenValid;
}
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
this._familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
this.loading = false;
});

View File

@@ -1,11 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { OrganizationPlansComponent } from "../../billing";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -17,15 +19,27 @@ import { SharedModule } from "../../shared";
templateUrl: "create-organization.component.html",
imports: [SharedModule, OrganizationPlansComponent, HeaderModule],
})
export class CreateOrganizationComponent {
export class CreateOrganizationComponent implements OnInit {
protected secretsManager = false;
protected plan: PlanType = PlanType.Free;
protected productTier: ProductTierType = ProductTierType.Free;
constructor(private route: ActivatedRoute) {
constructor(
private route: ActivatedRoute,
private configService: ConfigService,
) {}
async ngOnInit(): Promise<void> {
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
const familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => {
if (qParams.plan === "families" || qParams.productTier == ProductTierType.Families) {
this.plan = PlanType.FamiliesAnnually;
this.plan = familyPlan;
this.productTier = ProductTierType.Families;
} else if (qParams.plan === "teams" || qParams.productTier == ProductTierType.Teams) {
this.plan = PlanType.TeamsAnnually;

View File

@@ -2,7 +2,6 @@ import { TestBed } from "@angular/core/testing";
import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
@@ -12,6 +11,7 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { LogService } from "@bitwarden/logging";
@@ -36,13 +36,12 @@ describe("UpgradePaymentService", () => {
const mockAccountBillingClient = mock<AccountBillingClient>();
const mockTaxClient = mock<TaxClient>();
const mockLogService = mock<LogService>();
const mockApiService = mock<ApiService>();
const mockSyncService = mock<SyncService>();
const mockOrganizationService = mock<OrganizationService>();
const mockAccountService = mock<AccountService>();
const mockSubscriberBillingClient = mock<SubscriberBillingClient>();
const mockConfigService = mock<ConfigService>();
mockApiService.refreshIdentityToken.mockResolvedValue({});
mockSyncService.fullSync.mockResolvedValue(true);
let sut: UpgradePaymentService;
@@ -134,10 +133,10 @@ describe("UpgradePaymentService", () => {
{ provide: AccountBillingClient, useValue: mockAccountBillingClient },
{ provide: TaxClient, useValue: mockTaxClient },
{ provide: LogService, useValue: mockLogService },
{ provide: ApiService, useValue: mockApiService },
{ provide: SyncService, useValue: mockSyncService },
{ provide: OrganizationService, useValue: mockOrganizationService },
{ provide: AccountService, useValue: mockAccountService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
@@ -183,11 +182,11 @@ describe("UpgradePaymentService", () => {
mockAccountBillingClient,
mockTaxClient,
mockLogService,
mockApiService,
mockSyncService,
mockOrganizationService,
mockAccountService,
mockSubscriberBillingClient,
mockConfigService,
);
// Act & Assert
@@ -235,11 +234,11 @@ describe("UpgradePaymentService", () => {
mockAccountBillingClient,
mockTaxClient,
mockLogService,
mockApiService,
mockSyncService,
mockOrganizationService,
mockAccountService,
mockSubscriberBillingClient,
mockConfigService,
);
// Act & Assert
@@ -269,11 +268,11 @@ describe("UpgradePaymentService", () => {
mockAccountBillingClient,
mockTaxClient,
mockLogService,
mockApiService,
mockSyncService,
mockOrganizationService,
mockAccountService,
mockSubscriberBillingClient,
mockConfigService,
);
// Act & Assert
@@ -304,11 +303,11 @@ describe("UpgradePaymentService", () => {
mockAccountBillingClient,
mockTaxClient,
mockLogService,
mockApiService,
mockSyncService,
mockOrganizationService,
mockAccountService,
mockSubscriberBillingClient,
mockConfigService,
);
// Act & Assert
@@ -330,11 +329,11 @@ describe("UpgradePaymentService", () => {
mockAccountBillingClient,
mockTaxClient,
mockLogService,
mockApiService,
mockSyncService,
mockOrganizationService,
mockAccountService,
mockSubscriberBillingClient,
mockConfigService,
);
// Act & Assert
service?.accountCredit$.subscribe({
@@ -385,11 +384,11 @@ describe("UpgradePaymentService", () => {
mockAccountBillingClient,
mockTaxClient,
mockLogService,
mockApiService,
mockSyncService,
mockOrganizationService,
mockAccountService,
mockSubscriberBillingClient,
mockConfigService,
);
// Act & Assert
@@ -482,7 +481,6 @@ describe("UpgradePaymentService", () => {
mockTokenizedPaymentMethod,
mockBillingAddress,
);
expect(mockApiService.refreshIdentityToken).toHaveBeenCalled();
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
});
@@ -501,7 +499,6 @@ describe("UpgradePaymentService", () => {
accountCreditPaymentMethod,
mockBillingAddress,
);
expect(mockApiService.refreshIdentityToken).toHaveBeenCalled();
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
});
@@ -569,7 +566,7 @@ describe("UpgradePaymentService", () => {
billingEmail: "test@example.com",
},
plan: {
type: PlanType.FamiliesAnnually,
type: PlanType.FamiliesAnnually2025,
passwordManagerSeats: 6,
},
payment: {
@@ -582,10 +579,73 @@ describe("UpgradePaymentService", () => {
}),
"user-id",
);
expect(mockApiService.refreshIdentityToken).toHaveBeenCalled();
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
});
it("should use FamiliesAnnually2025 plan when feature flag is disabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(false);
mockOrganizationBillingService.purchaseSubscription.mockResolvedValue({
id: "org-id",
name: "Test Organization",
billingEmail: "test@example.com",
} as OrganizationResponse);
// Act
await sut.upgradeToFamilies(
mockAccount,
mockFamiliesPlanDetails,
mockTokenizedPaymentMethod,
{
organizationName: "Test Organization",
billingAddress: mockBillingAddress,
},
);
// Assert
expect(mockOrganizationBillingService.purchaseSubscription).toHaveBeenCalledWith(
expect.objectContaining({
plan: {
type: PlanType.FamiliesAnnually2025,
passwordManagerSeats: 6,
},
}),
"user-id",
);
});
it("should use FamiliesAnnually plan when feature flag is enabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(true);
mockOrganizationBillingService.purchaseSubscription.mockResolvedValue({
id: "org-id",
name: "Test Organization",
billingEmail: "test@example.com",
} as OrganizationResponse);
// Act
await sut.upgradeToFamilies(
mockAccount,
mockFamiliesPlanDetails,
mockTokenizedPaymentMethod,
{
organizationName: "Test Organization",
billingAddress: mockBillingAddress,
},
);
// Assert
expect(mockOrganizationBillingService.purchaseSubscription).toHaveBeenCalledWith(
expect.objectContaining({
plan: {
type: PlanType.FamiliesAnnually,
passwordManagerSeats: 6,
},
}),
"user-id",
);
});
it("should throw error if password manager seats are 0", async () => {
// Arrange
const invalidPlanDetails: PlanDetails = {

View File

@@ -1,7 +1,6 @@
import { Injectable } from "@angular/core";
import { defaultIfEmpty, find, map, mergeMap, Observable, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
@@ -17,6 +16,8 @@ import {
PersonalSubscriptionPricingTierId,
PersonalSubscriptionPricingTierIds,
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { LogService } from "@bitwarden/logging";
@@ -59,11 +60,11 @@ export class UpgradePaymentService {
private accountBillingClient: AccountBillingClient,
private taxClient: TaxClient,
private logService: LogService,
private apiService: ApiService,
private syncService: SyncService,
private organizationService: OrganizationService,
private accountService: AccountService,
private subscriberBillingClient: SubscriberBillingClient,
private configService: ConfigService,
) {}
userIsOwnerOfFreeOrg$: Observable<boolean> = this.accountService.activeAccount$.pipe(
@@ -169,6 +170,12 @@ export class UpgradePaymentService {
this.validatePaymentAndBillingInfo(paymentMethod, billingAddress);
const passwordManagerSeats = this.getPasswordManagerSeats(planDetails);
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
const familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
const subscriptionInformation: SubscriptionInformation = {
organization: {
@@ -176,7 +183,7 @@ export class UpgradePaymentService {
billingEmail: account.email, // Use account email as billing email
},
plan: {
type: PlanType.FamiliesAnnually,
type: familyPlan,
passwordManagerSeats: passwordManagerSeats,
},
payment: {
@@ -224,7 +231,6 @@ export class UpgradePaymentService {
}
private async refreshAndSync(): Promise<void> {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
}

View File

@@ -31,7 +31,9 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { PlanInterval, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
@@ -149,6 +151,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
protected estimatedTax: number = 0;
private _productTier = ProductTierType.Free;
private _familyPlan: PlanType;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@@ -247,6 +250,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
private subscriberBillingClient: SubscriberBillingClient,
private taxClient: TaxClient,
private organizationWarningsService: OrganizationWarningsService,
private configService: ConfigService,
) {}
async ngOnInit(): Promise<void> {
@@ -296,10 +300,16 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
}
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
this._familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) {
const upgradedPlan = this.passwordManagerPlans.find((plan) =>
this.currentPlan.productTier === ProductTierType.Free
? plan.type === PlanType.FamiliesAnnually
? plan.type === this._familyPlan
: plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1,
);
@@ -544,9 +554,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
if (this.acceptingSponsorship) {
const familyPlan = this.passwordManagerPlans.find(
(plan) => plan.type === PlanType.FamiliesAnnually,
);
const familyPlan = this.passwordManagerPlans.find((plan) => plan.type === this._familyPlan);
this.discount = familyPlan.PasswordManager.basePrice;
return [familyPlan];
}
@@ -562,6 +570,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
plan.productTier === ProductTierType.TeamsStarter ||
(this.selectedInterval === PlanInterval.Annually && plan.isAnnual) ||
(this.selectedInterval === PlanInterval.Monthly && !plan.isAnnual)) &&
(plan.productTier !== ProductTierType.Families || plan.type === this._familyPlan) &&
(!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) &&
this.planIsEnabled(plan),
);
@@ -926,7 +935,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) {
const upgradedPlan = this.passwordManagerPlans.find((plan) => {
if (this.currentPlan.productTier === ProductTierType.Free) {
return plan.type === PlanType.FamiliesAnnually;
return plan.type === this._familyPlan;
}
if (
@@ -1024,6 +1033,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
const getPlanFromLegacyEnum = (planType: PlanType): OrganizationSubscriptionPlan => {
switch (planType) {
case PlanType.FamiliesAnnually:
case PlanType.FamiliesAnnually2025:
return { tier: "families", cadence: "annually" };
case PlanType.TeamsMonthly:
return { tier: "teams", cadence: "monthly" };

View File

@@ -36,8 +36,10 @@ import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/commo
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -126,6 +128,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
private _productTier = ProductTierType.Free;
private _familyPlan: PlanType;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@@ -217,6 +220,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private accountService: AccountService,
private subscriberBillingClient: SubscriberBillingClient,
private taxClient: TaxClient,
private configService: ConfigService,
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
@@ -256,10 +260,16 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
}
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
this._familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) {
const upgradedPlan = this.passwordManagerPlans.find((plan) =>
this.currentPlan.productTier === ProductTierType.Free
? plan.type === PlanType.FamiliesAnnually
? plan.type === this._familyPlan
: plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1,
);
@@ -378,9 +388,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
get selectableProducts() {
if (this.acceptingSponsorship) {
const familyPlan = this.passwordManagerPlans.find(
(plan) => plan.type === PlanType.FamiliesAnnually,
);
const familyPlan = this.passwordManagerPlans.find((plan) => plan.type === this._familyPlan);
this.discount = familyPlan.PasswordManager.basePrice;
return [familyPlan];
}
@@ -397,6 +405,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
plan.productTier === ProductTierType.TeamsStarter) &&
(!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) &&
(!this.hasProvider || plan.productTier !== ProductTierType.TeamsStarter) &&
(plan.productTier !== ProductTierType.Families || plan.type === this._familyPlan) &&
((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) ||
(this.isProviderQualifiedFor2020Plan() &&
Allowed2020PlansForLegacyProviders.includes(plan.type))),
@@ -413,6 +422,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.passwordManagerPlans?.filter(
(plan) =>
plan.productTier === selectedProductTierType &&
(plan.productTier !== ProductTierType.Families || plan.type === this._familyPlan) &&
((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) ||
(this.isProviderQualifiedFor2020Plan() &&
Allowed2020PlansForLegacyProviders.includes(plan.type))),
@@ -713,6 +723,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private getPlanFromLegacyEnum(): OrganizationSubscriptionPlan {
switch (this.formGroup.value.plan) {
case PlanType.FamiliesAnnually:
case PlanType.FamiliesAnnually2025:
return { tier: "families", cadence: "annually" };
case PlanType.TeamsMonthly:
return { tier: "teams", cadence: "monthly" };
@@ -985,7 +996,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) {
const upgradedPlan = this.passwordManagerPlans.find((plan) => {
if (this.currentPlan.productTier === ProductTierType.Free) {
return plan.type === PlanType.FamiliesAnnually;
return plan.type === this._familyPlan;
}
if (

View File

@@ -300,6 +300,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
} else if (
this.sub.planType === PlanType.FamiliesAnnually ||
this.sub.planType === PlanType.FamiliesAnnually2025 ||
this.sub.planType === PlanType.FamiliesAnnually2019 ||
this.sub.planType === PlanType.TeamsStarter2023 ||
this.sub.planType === PlanType.TeamsStarter

View File

@@ -251,7 +251,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
this.loading = true;
let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website";
let plan: PlanInformation = {
type: this.getPlanType(),
type: await this.getPlanType(),
passwordManagerSeats: 1,
};
@@ -293,14 +293,21 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
this.verticalStepper.previous();
}
getPlanType() {
async getPlanType() {
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
const familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
switch (this.productTier) {
case ProductTierType.Teams:
return PlanType.TeamsAnnually;
case ProductTierType.Enterprise:
return PlanType.EnterpriseAnnually;
case ProductTierType.Families:
return PlanType.FamiliesAnnually;
return familyPlan;
case ProductTierType.Free:
return PlanType.Free;
default:

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
import { firstValueFrom, from, map, shareReplay } from "rxjs";
import { combineLatestWith, firstValueFrom, from, map, shareReplay } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
@@ -10,6 +10,8 @@ import {
SubscriptionInformation,
} from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { TaxClient } from "@bitwarden/web-vault/app/billing/clients";
import {
BillingAddressControls,
@@ -62,6 +64,7 @@ export class TrialBillingStepService {
private apiService: ApiService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private taxClient: TaxClient,
private configService: ConfigService,
) {}
private plans$ = from(this.apiService.getPlans()).pipe(
@@ -70,10 +73,17 @@ export class TrialBillingStepService {
getPrices$ = (product: Product, tier: Tier) =>
this.plans$.pipe(
map((plans) => {
combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.PM26462_Milestone_3)),
map(([plans, milestone3FeatureEnabled]) => {
switch (tier) {
case "families": {
const annually = plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually);
const annually = plans.data.find(
(plan) =>
plan.type ===
(milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025),
);
return {
annually: annually!.PasswordManager.basePrice,
};
@@ -149,9 +159,15 @@ export class TrialBillingStepService {
): Promise<OrganizationResponse> => {
const getPlanType = async (tier: Tier, cadence: Cadence) => {
const plans = await firstValueFrom(this.plans$);
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM26462_Milestone_3,
);
const familyPlan = milestone3FeatureEnabled
? PlanType.FamiliesAnnually
: PlanType.FamiliesAnnually2025;
switch (tier) {
case "families":
return plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually)!.type;
return plans.data.find((plan) => plan.type === familyPlan)!.type;
case "teams":
return plans.data.find(
(plan) =>