[PM-6827] Browser Extension Refresh - Tabs Routing (#9004)

* [PM-6827] Add componentRouteSwap util function

* [PM-6827] Add extension-refresh feature flag

* [PM-6827] Add extension-refresh route swap utils

* [PM-6827] Add the TabsV2 component

* [PM-6827] Add the TabsV2 to routing module

* [PM-6827] Fix route prefixes in popup-tab-navigation component
This commit is contained in:
Shane Melton 2024-05-06 09:14:47 -07:00 committed by GitHub
parent 09ff12fc02
commit ff3021129e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 125 additions and 8 deletions

View File

@ -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",
},

View File

@ -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,

View File

@ -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,

View File

@ -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<any>,
refreshedComponent: Type<any>,
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<boolean | UrlTree> {
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;
}
};
}

View File

@ -0,0 +1,11 @@
import { Component } from "@angular/core";
@Component({
selector: "app-tabs-v2",
template: `
<popup-tab-navigation>
<router-outlet></router-outlet>
</popup-tab-navigation>
`,
})
export class TabsV2Component {}

View File

@ -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<any>,
altComponent: Type<any>,
shouldSwapFn: () => Promise<boolean>,
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];
}

View File

@ -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<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;