diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index 3a275454d9..ced3f6462e 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -17,25 +17,25 @@ export class PopupTabNavigationComponent { navButtons = [ { label: "Vault", - page: "/vault", + page: "/tabs/vault", iconKey: "lock", iconKeyActive: "lock-f", }, { label: "Generator", - page: "/generator", + page: "/tabs/generator", iconKey: "generate", iconKeyActive: "generate-f", }, { label: "Send", - page: "/send", + page: "/tabs/send", iconKey: "send", iconKeyActive: "send-f", }, { label: "Settings", - page: "/settings", + page: "/tabs/settings", iconKey: "cog", iconKeyActive: "cog-f", }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 14659cb4df..0dcf496457 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -2,9 +2,9 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; import { - redirectGuard, AuthGuard, lockGuard, + redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; @@ -47,6 +47,7 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; import { FoldersComponent } from "./settings/folders.component"; @@ -54,6 +55,7 @@ import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component import { OptionsComponent } from "./settings/options.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; const unauthRouteOverrides = { @@ -322,9 +324,8 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "help-and-feedback" }, }, - { + ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", - component: TabsComponent, data: { state: "tabs" }, children: [ { @@ -336,6 +337,7 @@ const routes: Routes = [ path: "current", component: CurrentTabComponent, canActivate: [AuthGuard], + canMatch: [extensionRefreshRedirect("/tabs/vault")], data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, @@ -364,7 +366,7 @@ const routes: Routes = [ data: { state: "tabs_send" }, }, ], - }, + }), { path: "account-switcher", component: AccountSwitcherComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index a6e953ad1d..bed40dfddc 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -80,6 +80,7 @@ import { OptionsComponent } from "./settings/options.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; // Register the locales for the application @@ -160,6 +161,7 @@ import "../platform/popup/locales"; SsoComponent, SyncComponent, TabsComponent, + TabsV2Component, TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, diff --git a/apps/browser/src/popup/extension-refresh-route-utils.ts b/apps/browser/src/popup/extension-refresh-route-utils.ts new file mode 100644 index 0000000000..3c2ca33f86 --- /dev/null +++ b/apps/browser/src/popup/extension-refresh-route-utils.ts @@ -0,0 +1,45 @@ +import { inject, Type } from "@angular/core"; +import { Route, Router, Routes, UrlTree } from "@angular/router"; + +import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +/** + * Helper function to swap between two components based on the ExtensionRefresh feature flag. + * @param defaultComponent - The current non-refreshed component to render. + * @param refreshedComponent - The new refreshed component to render. + * @param options - The shared route options to apply to both components. + */ +export function extensionRefreshSwap( + defaultComponent: Type, + refreshedComponent: Type, + options: Route, +): Routes { + return componentRouteSwap( + defaultComponent, + refreshedComponent, + async () => { + const configService = inject(ConfigService); + return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + }, + options, + ); +} + +/** + * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag. + * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled. + */ +export function extensionRefreshRedirect(redirectUrl: string): () => Promise { + return async () => { + const configService = inject(ConfigService); + const router = inject(Router); + const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + if (shouldRedirect) { + return router.parseUrl(redirectUrl); + } else { + return true; + } + }; +} diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts new file mode 100644 index 0000000000..4cdb8fc029 --- /dev/null +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-tabs-v2", + template: ` + + + + `, +}) +export class TabsV2Component {} diff --git a/libs/angular/src/utils/component-route-swap.ts b/libs/angular/src/utils/component-route-swap.ts new file mode 100644 index 0000000000..1a2db317d6 --- /dev/null +++ b/libs/angular/src/utils/component-route-swap.ts @@ -0,0 +1,55 @@ +import { Type } from "@angular/core"; +import { Route, Routes } from "@angular/router"; + +/** + * Helper function to swap between two components based on an async condition. The async condition is evaluated + * as an `CanMatchFn` and supports Angular dependency injection via `inject()`. + * + * @example + * ```ts + * const routes = [ + * ...componentRouteSwap( + * defaultComponent, + * altComponent, + * async () => { + * const configService = inject(ConfigService); + * return configService.getFeatureFlag(FeatureFlag.SomeFlag); + * }, + * { + * path: 'some-path' + * } + * ), + * // Other routes... + * ]; + * ``` + * + * @param defaultComponent - The default component to render. + * @param altComponent - The alternate component to render when the condition is met. + * @param shouldSwapFn - The async function to determine if the alternate component should be rendered. + * @param options - The shared route options to apply to both components. + */ +export function componentRouteSwap( + defaultComponent: Type, + altComponent: Type, + shouldSwapFn: () => Promise, + options: Route, +): Routes { + const defaultRoute = { + ...options, + component: defaultComponent, + }; + + const altRoute: Route = { + ...options, + component: altComponent, + canMatch: [ + async () => { + return await shouldSwapFn(); + }, + ...(options.canMatch ?? []), + ], + }; + + // Return the alternate route first, so it is evaluated first. + return [altRoute, defaultRoute]; +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d84494362e..5ed3724f2f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -16,6 +16,7 @@ export enum FeatureFlag { AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", + ExtensionRefresh = "extension-refresh", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -42,6 +43,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.UnassignedItemsBanner]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, + [FeatureFlag.ExtensionRefresh]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;