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