diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index db3e69f7d6f..759ea80bc5f 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -17,6 +17,7 @@ import { import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { preventProdAccessGuard } from "@bitwarden/angular/platform/guard/prevent-prod-access.guard"; import { LoginComponent, LoginSecondaryContentComponent, @@ -141,6 +142,19 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ + { + path: "feature-flags", + canMatch: [preventProdAccessGuard], //preventSelfHostedAccessGuard + data: { + pageTitle: { + key: "featureFlags", + }, + maxWidth: "3xl", + hideIcon: true, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + loadComponent: () => + import("@bitwarden/angular/platform/feature-flags").then((m) => m.FeatureFlagsComponent), + }, { path: "signup", canActivate: [unauthGuardFn()], diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b5c34cc95a3..e0e40fb597d 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -308,6 +308,9 @@ export class AppComponent implements OnInit, OnDestroy { case "openPasswordHistory": await this.openGeneratorHistory(); break; + case "viewFeatureFlags": + await this.viewFeatureFlags(); + break; case "showToast": this.toastService._showToast(message); break; @@ -520,6 +523,10 @@ export class AppComponent implements OnInit, OnDestroy { return; } + async viewFeatureFlags() { + await this.router.navigate(["/feature-flags"]); + } + private async updateAppMenu() { let updateRequest: MenuUpdateRequest; const stateAccounts = await firstValueFrom(this.accountService.accounts$); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index a139c0c712c..fa7aff442b1 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1548,6 +1548,21 @@ "toggleDevTools": { "message": "Toggle developer tools" }, + "featureFlags": { + "message": "Feature flags" + }, + "flagName": { + "message": "Flag name" + }, + "flagValue": { + "message": "Flag value" + }, + "copyData": { + "message": "Copy data" + }, + "refresh": { + "message": "Refresh" + }, "minimize": { "message": "Minimize", "description": "Minimize window" diff --git a/apps/desktop/src/main/menu/menu.main.ts b/apps/desktop/src/main/menu/menu.main.ts index eafadf3bfb5..bee58b2bd2f 100644 --- a/apps/desktop/src/main/menu/menu.main.ts +++ b/apps/desktop/src/main/menu/menu.main.ts @@ -47,6 +47,7 @@ export class MenuMain { app.getVersion(), await firstValueFrom(this.desktopSettingsService.hardwareAcceleration$), this.versionMain, + await this.isProdOrSelfHost(), updateRequest, ).menu, ); @@ -57,6 +58,11 @@ export class MenuMain { return env.getWebVaultUrl() ?? cloudWebVaultUrl; } + private async isProdOrSelfHost(): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + return env.isProduction() || env.isSelfHosted(); + } + private initContextMenu() { if (this.windowMain.win == null) { return; diff --git a/apps/desktop/src/main/menu/menu.view.ts b/apps/desktop/src/main/menu/menu.view.ts index d967c72cc76..c6c65300d64 100644 --- a/apps/desktop/src/main/menu/menu.view.ts +++ b/apps/desktop/src/main/menu/menu.view.ts @@ -36,17 +36,29 @@ export class ViewMenu implements IMenubarMenu { items.push(this.toggleDevTools); } + // Desktop is considered self hosted argh. + // if (this._isProdOrSelfHost) { + items.push(this.featureFlags); + // } + return items; } private readonly _i18nService: I18nService; private readonly _messagingService: MessagingService; private readonly _isLocked: boolean; + private readonly _isProdOrSelfHost: boolean; - constructor(i18nService: I18nService, messagingService: MessagingService, isLocked: boolean) { + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isLocked: boolean, + isProdOrSelfHost: boolean, + ) { this._i18nService = i18nService; this._messagingService = messagingService; this._isLocked = isLocked; + this._isProdOrSelfHost = isProdOrSelfHost; } private get searchVault(): MenuItemConstructorOptions { @@ -134,13 +146,13 @@ export class ViewMenu implements IMenubarMenu { }; } - // private get viewFeatureFlags(): MenuItemConstructorOptions { - // return { - // id: "viewFeatureFlags", - // label: this.localize("viewFeatureFlags"), - // role: "viewFeatureFlags", - // }; - // } + private get featureFlags(): MenuItemConstructorOptions { + return { + id: "featureFlags", + label: this.localize("featureFlags"), + click: () => this.sendMessage("viewFeatureFlags"), + }; + } private localize(s: string) { return this._i18nService.t(s); diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index 825afdaa1e8..59f86d59a34 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -58,6 +58,7 @@ export class Menubar { appVersion: string, hardwareAccelerationEnabled: boolean, versionMain: VersionMain, + isProdOrSelfHost: boolean, updateRequest?: MenuUpdateRequest, ) { let isLocked = true; @@ -85,7 +86,7 @@ export class Menubar { isLockable, ), new EditMenu(i18nService, messagingService, isLocked), - new ViewMenu(i18nService, messagingService, isLocked), + new ViewMenu(i18nService, messagingService, isLocked, isProdOrSelfHost), new AccountMenu( i18nService, messagingService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 901376e0591..09d54c43bac 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -186,7 +186,7 @@ const routes: Routes = [ key: "featureFlags", }, maxWidth: "3xl", - hideIcon: true, // TODO: log bug with UIF or offer a PR to fix where this isn't reset to false upon navigation + hideIcon: true, } satisfies RouteDataProperties & AnonLayoutWrapperData, loadComponent: () => import("@bitwarden/angular/platform/feature-flags").then((m) => m.FeatureFlagsComponent),