Skip to content

Commit 14b2eb9

Browse files
authored
[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.
1 parent c7fa376 commit 14b2eb9

File tree

27 files changed

+67
-58
lines changed

27 files changed

+67
-58
lines changed

apps/browser/src/tools/background/fileless-importer.background.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
183183
return;
184184
}
185185

186-
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag<boolean>(
186+
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag(
187187
FeatureFlag.BrowserFilelessImport,
188188
);
189189
const userAuthStatus = await this.authService.getAuthStatus();

apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
5858

5959
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
6060
FeatureFlag.ShowPaymentMethodWarningBanners,
61-
false,
6261
);
6362

6463
constructor(

apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy {
218218
groups: groups$,
219219
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
220220
FeatureFlag.FlexibleCollectionsV1,
221-
false,
222221
),
223222
})
224223
.pipe(takeUntil(this.destroy$))

apps/web/src/app/admin-console/organizations/settings/account.component.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,10 @@ export class AccountComponent {
4444

4545
protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$(
4646
FeatureFlag.FlexibleCollectionsMigration,
47-
false,
4847
);
4948

5049
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
5150
FeatureFlag.FlexibleCollectionsV1,
52-
false,
5351
);
5452

5553
// FormGroup validators taken from server Organization domain object

apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class UserKeyRotationService {
9090
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
9191
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
9292

93-
if (await this.configService.getFeatureFlag<boolean>(FeatureFlag.KeyRotationImprovements)) {
93+
if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
9494
await this.apiService.postUserKeyUpdate(request);
9595
} else {
9696
await this.rotateUserKeyAndEncryptedDataLegacy(request);

apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
8484

8585
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
8686
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
87-
false,
8887
);
8988
}
9089

apps/web/src/app/layouts/user-layout.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class UserLayoutComponent implements OnInit {
4040

4141
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
4242
FeatureFlag.ShowPaymentMethodWarningBanners,
43-
false,
4443
);
4544

4645
constructor(

apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export enum CollectionDialogAction {
7676
})
7777
export class CollectionDialogComponent implements OnInit, OnDestroy {
7878
protected flexibleCollectionsV1Enabled$ = this.configService
79-
.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false)
79+
.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1)
8080
.pipe(first());
8181

8282
private destroy$ = new Subject<void>();

apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export class BulkDeleteDialogComponent {
5454

5555
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
5656
FeatureFlag.FlexibleCollectionsV1,
57-
false,
5857
);
5958

6059
constructor(

apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,8 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
6060
) {}
6161

6262
async ngOnInit() {
63-
this.showOnboardingAccess$ = await this.configService.getFeatureFlag$<boolean>(
63+
this.showOnboardingAccess$ = await this.configService.getFeatureFlag$(
6464
FeatureFlag.VaultOnboarding,
65-
false,
6665
);
6766
this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$;
6867
await this.setOnboardingTasks();

apps/web/src/app/vault/individual-vault/vault.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ export class VaultComponent implements OnInit, OnDestroy {
148148
protected currentSearchText$: Observable<string>;
149149
protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
150150
FeatureFlag.FlexibleCollectionsV1,
151-
false,
152151
);
153152

154153
private searchText$ = new Subject<string>();

apps/web/src/app/vault/org-vault/attachments.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
6060
async ngOnInit() {
6161
await super.ngOnInit();
6262
this.flexibleCollectionsV1Enabled = await firstValueFrom(
63-
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false),
63+
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
6464
);
6565
}
6666

apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,7 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni
7070
) {}
7171

7272
async ngOnInit() {
73-
const v1FCEnabled = await this.configService.getFeatureFlag(
74-
FeatureFlag.FlexibleCollectionsV1,
75-
false,
76-
);
73+
const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1);
7774
const org = await this.organizationService.get(this.params.organizationId);
7875

7976
if (org.canEditAllCiphers(v1FCEnabled)) {

apps/web/src/app/vault/org-vault/vault.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ export class VaultComponent implements OnInit, OnDestroy {
193193

194194
this._flexibleCollectionsV1FlagEnabled = await this.configService.getFeatureFlag(
195195
FeatureFlag.FlexibleCollectionsV1,
196-
false,
197196
);
198197

199198
const filter$ = this.routedVaultFilterService.filter$;

bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export class ClientsComponent extends BaseClientsComponent {
4242

4343
protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
4444
FeatureFlag.EnableConsolidatedBilling,
45-
false,
4645
);
4746

4847
constructor(

bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,10 @@ export class ProvidersLayoutComponent {
3737

3838
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
3939
FeatureFlag.ShowPaymentMethodWarningBanners,
40-
false,
4140
);
4241

4342
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
4443
FeatureFlag.EnableConsolidatedBilling,
45-
false,
4644
);
4745

4846
constructor(

bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export class AccountComponent {
3030

3131
protected enableDeleteProvider$ = this.configService.getFeatureFlag$(
3232
FeatureFlag.EnableDeleteProvider,
33-
false,
3433
);
3534

3635
constructor(

bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@ export class SetupComponent implements OnInit {
3636

3737
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
3838
FeatureFlag.ShowPaymentMethodWarningBanners,
39-
false,
4039
);
4140

4241
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
4342
FeatureFlag.EnableConsolidatedBilling,
44-
false,
4543
);
4644

4745
constructor(

libs/angular/src/directives/if-feature.directive.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
33
import { By } from "@angular/platform-browser";
44
import { mock, MockProxy } from "jest-mock-extended";
55

6-
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
6+
import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
77
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
88
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
99

@@ -41,10 +41,8 @@ describe("IfFeatureDirective", () => {
4141
let content: HTMLElement;
4242
let mockConfigService: MockProxy<ConfigService>;
4343

44-
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => {
45-
mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) =>
46-
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
47-
);
44+
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: AllowedFeatureFlagTypes) => {
45+
mockConfigService.getFeatureFlag.mockImplementation((f) => Promise.resolve(flagValue as any));
4846
};
4947

5048
const queryContent = (testId: string) =>

libs/angular/src/directives/if-feature.directive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
22

3-
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
3+
import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
44
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
55
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
66

@@ -23,7 +23,7 @@ export class IfFeatureDirective implements OnInit {
2323
* Optional value to compare against the value of the feature flag in the config service.
2424
* @default true
2525
*/
26-
@Input() appIfFeatureValue: FeatureFlagValue = true;
26+
@Input() appIfFeatureValue: AllowedFeatureFlagTypes = true;
2727

2828
private hasView = false;
2929

libs/angular/src/platform/guard/feature-flag.guard.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ describe("canAccessFeature", () => {
3434
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
3535
);
3636
} else if (typeof flagValue === "string") {
37-
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") =>
38-
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
37+
mockConfigService.getFeatureFlag.mockImplementation((flag) =>
38+
flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(""),
3939
);
4040
} else if (typeof flagValue === "number") {
41-
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) =>
42-
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue),
41+
mockConfigService.getFeatureFlag.mockImplementation((flag) =>
42+
flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(0),
4343
);
4444
}
4545

libs/angular/src/vault/components/add-edit.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
182182
async ngOnInit() {
183183
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
184184
FeatureFlag.FlexibleCollectionsV1,
185-
false,
186185
);
187186

188187
this.policyService

libs/common/src/enums/feature-flag.enum.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* Feature flags.
3+
*
4+
* Flags MUST be short lived and SHALL be removed once enabled.
5+
*/
16
export enum FeatureFlag {
27
BrowserFilelessImport = "browser-fileless-import",
38
ItemShare = "item-share",
@@ -13,5 +18,32 @@ export enum FeatureFlag {
1318
EnableDeleteProvider = "AC-1218-delete-provider",
1419
}
1520

16-
// Replace this with a type safe lookup of the feature flag values in PM-2282
17-
export type FeatureFlagValue = number | string | boolean;
21+
export type AllowedFeatureFlagTypes = boolean | number | string;
22+
23+
// Helper to ensure the value is treated as a boolean.
24+
const FALSE = false as boolean;
25+
26+
/**
27+
* Default value for feature flags.
28+
*
29+
* DO NOT enable previously disabled flags, REMOVE them instead.
30+
* We support true as a value as we prefer flags to "enable" not "disable".
31+
*/
32+
export const DefaultFeatureFlagValue = {
33+
[FeatureFlag.BrowserFilelessImport]: FALSE,
34+
[FeatureFlag.ItemShare]: FALSE,
35+
[FeatureFlag.FlexibleCollectionsV1]: FALSE,
36+
[FeatureFlag.VaultOnboarding]: FALSE,
37+
[FeatureFlag.GeneratorToolsModernization]: FALSE,
38+
[FeatureFlag.KeyRotationImprovements]: FALSE,
39+
[FeatureFlag.FlexibleCollectionsMigration]: FALSE,
40+
[FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE,
41+
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
42+
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
43+
[FeatureFlag.UnassignedItemsBanner]: FALSE,
44+
[FeatureFlag.EnableDeleteProvider]: FALSE,
45+
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
46+
47+
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
48+
49+
export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag];

libs/common/src/platform/abstractions/config/config.service.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Observable } from "rxjs";
22
import { SemVer } from "semver";
33

4-
import { FeatureFlag } from "../../../enums/feature-flag.enum";
4+
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
55
import { Region } from "../environment.service";
66

77
import { ServerConfig } from "./server-config";
@@ -14,23 +14,15 @@ export abstract class ConfigService {
1414
/**
1515
* Retrieves the value of a feature flag for the currently active user
1616
* @param key The feature flag to retrieve
17-
* @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable
1817
* @returns An observable that emits the value of the feature flag, updates as the server config changes
1918
*/
20-
getFeatureFlag$: <T extends boolean | number | string>(
21-
key: FeatureFlag,
22-
defaultValue?: T,
23-
) => Observable<T>;
19+
getFeatureFlag$: <Flag extends FeatureFlag>(key: Flag) => Observable<FeatureFlagValueType<Flag>>;
2420
/**
2521
* Retrieves the value of a feature flag for the currently active user
2622
* @param key The feature flag to retrieve
27-
* @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable
2823
* @returns The value of the feature flag
2924
*/
30-
getFeatureFlag: <T extends boolean | number | string>(
31-
key: FeatureFlag,
32-
defaultValue?: T,
33-
) => Promise<T>;
25+
getFeatureFlag: <Flag extends FeatureFlag>(key: Flag) => Promise<FeatureFlagValueType<Flag>>;
3426
/**
3527
* Verifies whether the server version meets the minimum required version
3628
* @param minimumRequiredServerVersion The minimum version required

libs/common/src/platform/abstractions/config/server-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Jsonify } from "type-fest";
22

3+
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
34
import {
45
ServerConfigData,
56
ThirdPartyServerConfigData,
@@ -14,7 +15,7 @@ export class ServerConfig {
1415
server?: ThirdPartyServerConfigData;
1516
environment?: EnvironmentServerConfigData;
1617
utcDate: Date;
17-
featureStates: { [key: string]: string } = {};
18+
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
1819

1920
constructor(serverConfigData: ServerConfigData) {
2021
this.version = serverConfigData.version;

libs/common/src/platform/models/data/server-config.data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Jsonify } from "type-fest";
22

3+
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
34
import { Region } from "../../abstractions/environment.service";
45
import {
56
ServerConfigResponse,
@@ -13,7 +14,7 @@ export class ServerConfigData {
1314
server?: ThirdPartyServerConfigData;
1415
environment?: EnvironmentServerConfigData;
1516
utcDate: string;
16-
featureStates: { [key: string]: string } = {};
17+
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
1718

1819
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
1920
this.version = serverConfigResponse?.version;

libs/common/src/platform/services/config/default-config.service.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import {
1313
} from "rxjs";
1414
import { SemVer } from "semver";
1515

16-
import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum";
16+
import {
17+
DefaultFeatureFlagValue,
18+
FeatureFlag,
19+
FeatureFlagValueType,
20+
} from "../../../enums/feature-flag.enum";
1721
import { UserId } from "../../../types/guid";
1822
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
1923
import { ConfigService } from "../../abstractions/config/config.service";
@@ -89,20 +93,21 @@ export class DefaultConfigService implements ConfigService {
8993
map((config) => config?.environment?.cloudRegion ?? Region.US),
9094
);
9195
}
92-
getFeatureFlag$<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) {
96+
97+
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
9398
return this.serverConfig$.pipe(
9499
map((serverConfig) => {
95100
if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) {
96-
return defaultValue;
101+
return DefaultFeatureFlagValue[key];
97102
}
98103

99-
return serverConfig.featureStates[key] as T;
104+
return serverConfig.featureStates[key] as FeatureFlagValueType<Flag>;
100105
}),
101106
);
102107
}
103108

104-
async getFeatureFlag<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) {
105-
return await firstValueFrom(this.getFeatureFlag$(key, defaultValue));
109+
async getFeatureFlag<Flag extends FeatureFlag>(key: Flag) {
110+
return await firstValueFrom(this.getFeatureFlag$(key));
106111
}
107112

108113
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) {

0 commit comments

Comments
 (0)