1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 19:34:03 +00:00

[PM-29087] [PM-29088] Remove FF: pm-26793-fetch-premium-price-from-pricing-service - Logic + Flag (#18946)

* refactor(billing): remove PM-26793 feature flag from subscription pricing service

* test(billing): update subscription pricing tests for PM-26793 feature flag removal

* chore: remove PM-26793 feature flag from keys
This commit is contained in:
Alex Morask
2026-02-19 09:26:18 -06:00
committed by GitHub
parent c8ba23e28d
commit 4f256fee6d
3 changed files with 30 additions and 114 deletions

View File

@@ -231,6 +231,7 @@ describe("DefaultSubscriptionPricingService", () => {
},
storage: {
price: 4,
provided: 1,
},
} as PremiumPlanResponse;
@@ -350,7 +351,7 @@ describe("DefaultSubscriptionPricingService", () => {
billingApiService.getPlans.mockResolvedValue(mockPlansResponse);
billingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
configService.getFeatureFlag$.mockReturnValue(of(false)); // Default to false (use hardcoded value)
configService.getFeatureFlag$.mockReturnValue(of(false));
setupEnvironmentService(environmentService);
service = new DefaultSubscriptionPricingService(
@@ -915,7 +916,7 @@ describe("DefaultSubscriptionPricingService", () => {
const testError = new Error("Premium plan API error");
errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
errorBillingApiService.getPremiumPlan.mockRejectedValue(testError);
errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag to use premium plan API
errorConfigService.getFeatureFlag$.mockReturnValue(of(false));
setupEnvironmentService(errorEnvironmentService);
const errorService = new DefaultSubscriptionPricingService(
@@ -959,71 +960,16 @@ describe("DefaultSubscriptionPricingService", () => {
expect(getPlansResponse).toHaveBeenCalledTimes(1);
});
it("should share premium plan API response between multiple subscriptions when feature flag is enabled", () => {
// Create a new mock to avoid conflicts with beforeEach setup
const newBillingApiService = mock<BillingApiServiceAbstraction>();
const newConfigService = mock<ConfigService>();
const newEnvironmentService = mock<EnvironmentService>();
newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
newBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse);
newConfigService.getFeatureFlag$.mockReturnValue(of(true));
setupEnvironmentService(newEnvironmentService);
const getPremiumPlanSpy = jest.spyOn(newBillingApiService, "getPremiumPlan");
// Create a new service instance with the feature flag enabled
const newService = new DefaultSubscriptionPricingService(
newBillingApiService,
newConfigService,
i18nService,
logService,
newEnvironmentService,
);
it("should share premium plan API response between multiple subscriptions", () => {
const getPremiumPlanSpy = jest.spyOn(billingApiService, "getPremiumPlan");
// Subscribe to the premium pricing tier multiple times
newService.getPersonalSubscriptionPricingTiers$().subscribe();
newService.getPersonalSubscriptionPricingTiers$().subscribe();
service.getPersonalSubscriptionPricingTiers$().subscribe();
service.getPersonalSubscriptionPricingTiers$().subscribe();
// API should only be called once due to shareReplay on premiumPlanResponse$
expect(getPremiumPlanSpy).toHaveBeenCalledTimes(1);
});
it("should use hardcoded premium price when feature flag is disabled", (done) => {
// Create a new mock to test from scratch
const newBillingApiService = mock<BillingApiServiceAbstraction>();
const newConfigService = mock<ConfigService>();
const newEnvironmentService = mock<EnvironmentService>();
newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse);
newBillingApiService.getPremiumPlan.mockResolvedValue({
seat: { price: 999 }, // Different price to verify hardcoded value is used
storage: { price: 999 },
} as PremiumPlanResponse);
newConfigService.getFeatureFlag$.mockReturnValue(of(false));
setupEnvironmentService(newEnvironmentService);
// Create a new service instance with the feature flag disabled
const newService = new DefaultSubscriptionPricingService(
newBillingApiService,
newConfigService,
i18nService,
logService,
newEnvironmentService,
);
// Subscribe with feature flag disabled
newService.getPersonalSubscriptionPricingTiers$().subscribe((tiers) => {
const premiumTier = tiers.find(
(tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium,
);
// Should use hardcoded value of 10, not the API response value of 999
expect(premiumTier!.passwordManager.annualPrice).toBe(10);
expect(premiumTier!.passwordManager.annualPricePerAdditionalStorageGB).toBe(4);
done();
});
});
});
describe("Self-hosted environment behavior", () => {
@@ -1035,7 +981,7 @@ describe("DefaultSubscriptionPricingService", () => {
const getPlansSpy = jest.spyOn(selfHostedBillingApiService, "getPlans");
const getPremiumPlanSpy = jest.spyOn(selfHostedBillingApiService, "getPremiumPlan");
selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true));
selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(false));
setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted);
const selfHostedService = new DefaultSubscriptionPricingService(
@@ -1061,7 +1007,7 @@ describe("DefaultSubscriptionPricingService", () => {
const selfHostedConfigService = mock<ConfigService>();
const selfHostedEnvironmentService = mock<EnvironmentService>();
selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true));
selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(false));
setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted);
const selfHostedService = new DefaultSubscriptionPricingService(

View File

@@ -33,16 +33,6 @@ import {
} from "../types/subscription-pricing-tier";
export class DefaultSubscriptionPricingService implements SubscriptionPricingServiceAbstraction {
/**
* Fallback premium pricing used when the feature flag is disabled.
* These values represent the legacy pricing model and will not reflect
* server-side price changes. They are retained for backward compatibility
* during the feature flag rollout period.
*/
private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10;
private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4;
private static readonly FALLBACK_PREMIUM_PROVIDED_STORAGE_GB = 1;
constructor(
private billingApiService: BillingApiServiceAbstraction,
private configService: ConfigService,
@@ -123,45 +113,27 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
shareReplay({ bufferSize: 1, refCount: false }),
);
private premium$: Observable<PersonalSubscriptionPricingTier> = this.configService
.getFeatureFlag$(FeatureFlag.PM26793_FetchPremiumPriceFromPricingService)
.pipe(
take(1), // Lock behavior at first subscription to prevent switching data sources mid-stream
switchMap((fetchPremiumFromPricingService) =>
fetchPremiumFromPricingService
? this.premiumPlanResponse$.pipe(
map((premiumPlan) => ({
seat: premiumPlan.seat?.price,
storage: premiumPlan.storage?.price,
provided: premiumPlan.storage?.provided,
})),
)
: of({
seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE,
storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE,
provided: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_PROVIDED_STORAGE_GB,
}),
),
map((premiumPrices) => ({
id: PersonalSubscriptionPricingTierIds.Premium,
name: this.i18nService.t("premium"),
description: this.i18nService.t("advancedOnlineSecurity"),
availableCadences: [SubscriptionCadenceIds.Annually],
passwordManager: {
type: "standalone",
annualPrice: premiumPrices.seat,
annualPricePerAdditionalStorageGB: premiumPrices.storage,
providedStorageGB: premiumPrices.provided,
features: [
this.featureTranslations.builtInAuthenticator(),
this.featureTranslations.secureFileStorage(),
this.featureTranslations.emergencyAccess(),
this.featureTranslations.breachMonitoring(),
this.featureTranslations.andMoreFeatures(),
],
},
})),
);
private premium$: Observable<PersonalSubscriptionPricingTier> = this.premiumPlanResponse$.pipe(
map((premiumPlan) => ({
id: PersonalSubscriptionPricingTierIds.Premium,
name: this.i18nService.t("premium"),
description: this.i18nService.t("advancedOnlineSecurity"),
availableCadences: [SubscriptionCadenceIds.Annually],
passwordManager: {
type: "standalone",
annualPrice: premiumPlan.seat?.price,
annualPricePerAdditionalStorageGB: premiumPlan.storage?.price,
providedStorageGB: premiumPlan.storage?.provided,
features: [
this.featureTranslations.builtInAuthenticator(),
this.featureTranslations.secureFileStorage(),
this.featureTranslations.emergencyAccess(),
this.featureTranslations.breachMonitoring(),
this.featureTranslations.andMoreFeatures(),
],
},
})),
);
private families$: Observable<PersonalSubscriptionPricingTier> =
this.organizationPlansResponse$.pipe(

View File

@@ -31,7 +31,6 @@ export enum FeatureFlag {
/* Billing */
TrialPaymentOptional = "PM-8163-trial-payment",
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service",
PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog",
PM26462_Milestone_3 = "pm-26462-milestone-3",
PM23341_Milestone_2 = "pm-23341-milestone-2",
@@ -146,7 +145,6 @@ export const DefaultFeatureFlagValue = {
/* Billing */
[FeatureFlag.TrialPaymentOptional]: FALSE,
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
[FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE,
[FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE,
[FeatureFlag.PM26462_Milestone_3]: FALSE,
[FeatureFlag.PM23341_Milestone_2]: FALSE,