mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-2282] Make feature flags type safe (#8612)
Refactors the feature flags in ConfigService to be type safe. It also moves the default value to a centralized location rather than the caller defining it. This ensures consistency across the various places they are used.
This commit is contained in:
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -41,10 +41,8 @@ describe("IfFeatureDirective", () => {
|
||||
let content: HTMLElement;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
|
||||
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
|
||||
);
|
||||
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: AllowedFeatureFlagTypes) => {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((f) => Promise.resolve(flagValue as any));
|
||||
};
|
||||
|
||||
const queryContent = (testId: string) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -23,7 +23,7 @@ export class IfFeatureDirective implements OnInit {
|
||||
* Optional value to compare against the value of the feature flag in the config service.
|
||||
* @default true
|
||||
*/
|
||||
@Input() appIfFeatureValue: FeatureFlagValue = true;
|
||||
@Input() appIfFeatureValue: AllowedFeatureFlagTypes = true;
|
||||
|
||||
private hasView = false;
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ describe("canAccessFeature", () => {
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
|
||||
);
|
||||
} else if (typeof flagValue === "string") {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(""),
|
||||
);
|
||||
} else if (typeof flagValue === "number") {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(0),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
|
||||
this.policyService
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Feature flags.
|
||||
*
|
||||
* Flags MUST be short lived and SHALL be removed once enabled.
|
||||
*/
|
||||
export enum FeatureFlag {
|
||||
BrowserFilelessImport = "browser-fileless-import",
|
||||
ItemShare = "item-share",
|
||||
@@ -13,5 +18,32 @@ export enum FeatureFlag {
|
||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||
}
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
export type FeatureFlagValue = number | string | boolean;
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
|
||||
// Helper to ensure the value is treated as a boolean.
|
||||
const FALSE = false as boolean;
|
||||
|
||||
/**
|
||||
* Default value for feature flags.
|
||||
*
|
||||
* DO NOT enable previously disabled flags, REMOVE them instead.
|
||||
* We support true as a value as we prefer flags to "enable" not "disable".
|
||||
*/
|
||||
export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.BrowserFilelessImport]: FALSE,
|
||||
[FeatureFlag.ItemShare]: FALSE,
|
||||
[FeatureFlag.FlexibleCollectionsV1]: FALSE,
|
||||
[FeatureFlag.VaultOnboarding]: FALSE,
|
||||
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
||||
[FeatureFlag.KeyRotationImprovements]: FALSE,
|
||||
[FeatureFlag.FlexibleCollectionsMigration]: FALSE,
|
||||
[FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE,
|
||||
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
|
||||
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
||||
[FeatureFlag.UnassignedItemsBanner]: FALSE,
|
||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
|
||||
import { Region } from "../environment.service";
|
||||
|
||||
import { ServerConfig } from "./server-config";
|
||||
@@ -14,23 +14,15 @@ export abstract class ConfigService {
|
||||
/**
|
||||
* Retrieves the value of a feature flag for the currently active user
|
||||
* @param key The feature flag to retrieve
|
||||
* @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable
|
||||
* @returns An observable that emits the value of the feature flag, updates as the server config changes
|
||||
*/
|
||||
getFeatureFlag$: <T extends boolean | number | string>(
|
||||
key: FeatureFlag,
|
||||
defaultValue?: T,
|
||||
) => Observable<T>;
|
||||
getFeatureFlag$: <Flag extends FeatureFlag>(key: Flag) => Observable<FeatureFlagValueType<Flag>>;
|
||||
/**
|
||||
* Retrieves the value of a feature flag for the currently active user
|
||||
* @param key The feature flag to retrieve
|
||||
* @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable
|
||||
* @returns The value of the feature flag
|
||||
*/
|
||||
getFeatureFlag: <T extends boolean | number | string>(
|
||||
key: FeatureFlag,
|
||||
defaultValue?: T,
|
||||
) => Promise<T>;
|
||||
getFeatureFlag: <Flag extends FeatureFlag>(key: Flag) => Promise<FeatureFlagValueType<Flag>>;
|
||||
/**
|
||||
* Verifies whether the server version meets the minimum required version
|
||||
* @param minimumRequiredServerVersion The minimum version required
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
|
||||
import {
|
||||
ServerConfigData,
|
||||
ThirdPartyServerConfigData,
|
||||
@@ -14,7 +15,7 @@ export class ServerConfig {
|
||||
server?: ThirdPartyServerConfigData;
|
||||
environment?: EnvironmentServerConfigData;
|
||||
utcDate: Date;
|
||||
featureStates: { [key: string]: string } = {};
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
|
||||
constructor(serverConfigData: ServerConfigData) {
|
||||
this.version = serverConfigData.version;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
import {
|
||||
ServerConfigResponse,
|
||||
@@ -13,7 +14,7 @@ export class ServerConfigData {
|
||||
server?: ThirdPartyServerConfigData;
|
||||
environment?: EnvironmentServerConfigData;
|
||||
utcDate: string;
|
||||
featureStates: { [key: string]: string } = {};
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
|
||||
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
|
||||
this.version = serverConfigResponse?.version;
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
} from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum";
|
||||
import {
|
||||
DefaultFeatureFlagValue,
|
||||
FeatureFlag,
|
||||
FeatureFlagValueType,
|
||||
} from "../../../enums/feature-flag.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
@@ -89,20 +93,21 @@ export class DefaultConfigService implements ConfigService {
|
||||
map((config) => config?.environment?.cloudRegion ?? Region.US),
|
||||
);
|
||||
}
|
||||
getFeatureFlag$<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) {
|
||||
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
|
||||
return this.serverConfig$.pipe(
|
||||
map((serverConfig) => {
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) {
|
||||
return defaultValue;
|
||||
return DefaultFeatureFlagValue[key];
|
||||
}
|
||||
|
||||
return serverConfig.featureStates[key] as T;
|
||||
return serverConfig.featureStates[key] as FeatureFlagValueType<Flag>;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureFlag<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) {
|
||||
return await firstValueFrom(this.getFeatureFlag$(key, defaultValue));
|
||||
async getFeatureFlag<Flag extends FeatureFlag>(key: Flag) {
|
||||
return await firstValueFrom(this.getFeatureFlag$(key));
|
||||
}
|
||||
|
||||
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) {
|
||||
|
||||
Reference in New Issue
Block a user