diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts
index b6e86ba19ff..8fab7df1cd8 100644
--- a/apps/desktop/src/app/app-routing.module.ts
+++ b/apps/desktop/src/app/app-routing.module.ts
@@ -14,6 +14,7 @@ import {
} from "@bitwarden/angular/auth/guards";
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 {
DevicesIcon,
RegistrationUserAddIcon,
@@ -39,15 +40,19 @@ import {
TwoFactorAuthGuard,
NewDeviceVerificationComponent,
} from "@bitwarden/auth/angular";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
+import { VaultComponent } from "../vault/app/vault-v3/vault.component";
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
+import { DesktopLayoutComponent } from "./layout/desktop-layout.component";
import { SendComponent } from "./tools/send/send.component";
+import { SendV2Component } from "./tools/send-v2/send-v2.component";
/**
* Data properties acceptable for use in route objects in the desktop
@@ -99,7 +104,10 @@ const routes: Routes = [
{
path: "vault",
component: VaultV2Component,
- canActivate: [authGuard],
+ canActivate: [
+ authGuard,
+ canAccessFeature(FeatureFlag.DesktopUiMigrationMilestone1, false, "new-vault", false),
+ ],
},
{
path: "send",
@@ -325,6 +333,21 @@ const routes: Routes = [
},
],
},
+ {
+ path: "",
+ component: DesktopLayoutComponent,
+ canActivate: [authGuard],
+ children: [
+ {
+ path: "new-vault",
+ component: VaultComponent,
+ },
+ {
+ path: "new-sends",
+ component: SendV2Component,
+ },
+ ],
+ },
];
@NgModule({
diff --git a/apps/desktop/src/app/layout/desktop-layout.component.html b/apps/desktop/src/app/layout/desktop-layout.component.html
new file mode 100644
index 00000000000..94b9201ae21
--- /dev/null
+++ b/apps/desktop/src/app/layout/desktop-layout.component.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/app/layout/desktop-layout.component.spec.ts b/apps/desktop/src/app/layout/desktop-layout.component.spec.ts
new file mode 100644
index 00000000000..cc2f7e58dfb
--- /dev/null
+++ b/apps/desktop/src/app/layout/desktop-layout.component.spec.ts
@@ -0,0 +1,61 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { RouterModule } from "@angular/router";
+import { mock } from "jest-mock-extended";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { NavigationModule } from "@bitwarden/components";
+
+import { DesktopLayoutComponent } from "./desktop-layout.component";
+
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: jest.fn().mockImplementation((query) => ({
+ matches: true,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ })),
+});
+
+describe("DesktopLayoutComponent", () => {
+ let component: DesktopLayoutComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [DesktopLayoutComponent, RouterModule.forRoot([]), NavigationModule],
+ providers: [
+ {
+ provide: I18nService,
+ useValue: mock(),
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(DesktopLayoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("creates component", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("renders bit-layout component", () => {
+ const compiled = fixture.nativeElement;
+ const layoutElement = compiled.querySelector("bit-layout");
+
+ expect(layoutElement).toBeTruthy();
+ });
+
+ it("supports content projection for side-nav", () => {
+ const compiled = fixture.nativeElement;
+ const ngContent = compiled.querySelectorAll("ng-content");
+
+ expect(ngContent).toBeTruthy();
+ });
+});
diff --git a/apps/desktop/src/app/layout/desktop-layout.component.ts b/apps/desktop/src/app/layout/desktop-layout.component.ts
new file mode 100644
index 00000000000..5059a6e4d0b
--- /dev/null
+++ b/apps/desktop/src/app/layout/desktop-layout.component.ts
@@ -0,0 +1,18 @@
+import { ChangeDetectionStrategy, Component } from "@angular/core";
+import { RouterModule } from "@angular/router";
+
+import { PasswordManagerLogo } from "@bitwarden/assets/svg";
+import { LayoutComponent, NavigationModule } from "@bitwarden/components";
+import { I18nPipe } from "@bitwarden/ui-common";
+
+import { DesktopSideNavComponent } from "./desktop-side-nav.component";
+
+@Component({
+ selector: "app-layout",
+ imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent],
+ templateUrl: "./desktop-layout.component.html",
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DesktopLayoutComponent {
+ protected readonly logo = PasswordManagerLogo;
+}
diff --git a/apps/desktop/src/app/layout/desktop-side-nav.component.html b/apps/desktop/src/app/layout/desktop-side-nav.component.html
new file mode 100644
index 00000000000..ede3f9131b7
--- /dev/null
+++ b/apps/desktop/src/app/layout/desktop-side-nav.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts b/apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts
new file mode 100644
index 00000000000..59e743f430a
--- /dev/null
+++ b/apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts
@@ -0,0 +1,74 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { mock } from "jest-mock-extended";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { NavigationModule } from "@bitwarden/components";
+
+import { DesktopSideNavComponent } from "./desktop-side-nav.component";
+
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: jest.fn().mockImplementation((query) => ({
+ matches: true,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ })),
+});
+
+describe("DesktopSideNavComponent", () => {
+ let component: DesktopSideNavComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [DesktopSideNavComponent, NavigationModule],
+ providers: [
+ {
+ provide: I18nService,
+ useValue: mock(),
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(DesktopSideNavComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("creates component", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("renders bit-side-nav component", () => {
+ const compiled = fixture.nativeElement;
+ const sideNavElement = compiled.querySelector("bit-side-nav");
+
+ expect(sideNavElement).toBeTruthy();
+ });
+
+ it("uses primary variant by default", () => {
+ expect(component.variant()).toBe("primary");
+ });
+
+ it("accepts variant input", () => {
+ fixture.componentRef.setInput("variant", "secondary");
+ fixture.detectChanges();
+
+ expect(component.variant()).toBe("secondary");
+ });
+
+ it("passes variant to bit-side-nav", () => {
+ fixture.componentRef.setInput("variant", "secondary");
+ fixture.detectChanges();
+
+ const compiled = fixture.nativeElement;
+ const sideNavElement = compiled.querySelector("bit-side-nav");
+
+ expect(sideNavElement.getAttribute("ng-reflect-variant")).toBe("secondary");
+ });
+});
diff --git a/apps/desktop/src/app/layout/desktop-side-nav.component.ts b/apps/desktop/src/app/layout/desktop-side-nav.component.ts
new file mode 100644
index 00000000000..b0d9fd16fcc
--- /dev/null
+++ b/apps/desktop/src/app/layout/desktop-side-nav.component.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from "@angular/common";
+import { ChangeDetectionStrategy, Component, input } from "@angular/core";
+
+import { NavigationModule, SideNavVariant } from "@bitwarden/components";
+
+@Component({
+ selector: "app-side-nav",
+ templateUrl: "desktop-side-nav.component.html",
+ imports: [CommonModule, NavigationModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DesktopSideNavComponent {
+ readonly variant = input("primary");
+}
diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts
new file mode 100644
index 00000000000..8055bc07667
--- /dev/null
+++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { SendV2Component } from "./send-v2.component";
+
+describe("SendV2Component", () => {
+ let component: SendV2Component;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [SendV2Component],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(SendV2Component);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("creates component", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts
new file mode 100644
index 00000000000..4840cd4cce8
--- /dev/null
+++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts
@@ -0,0 +1,9 @@
+import { Component, ChangeDetectionStrategy } from "@angular/core";
+
+@Component({
+ selector: "app-send-v2",
+ imports: [],
+ template: "Sends V2 Component
",
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SendV2Component {}
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 6bef882d970..f6f078611c9 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -2228,6 +2228,10 @@
"contactInfo": {
"message": "Contact information"
},
+ "send": {
+ "message": "Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
"allSends": {
"message": "All Sends",
"description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -2991,7 +2995,8 @@
"message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected."
},
"vault": {
- "message": "Vault"
+ "message": "Vault",
+ "description": "'Vault' is a noun and refers to the Bitwarden Vault feature."
},
"loginWithMasterPassword": {
"message": "Log in with master password"
diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts
new file mode 100644
index 00000000000..89ba05055f8
--- /dev/null
+++ b/apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { VaultComponent } from "./vault.component";
+
+describe("VaultComponent", () => {
+ let component: VaultComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [VaultComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(VaultComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("creates component", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts
new file mode 100644
index 00000000000..b29b66225c7
--- /dev/null
+++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts
@@ -0,0 +1,9 @@
+import { ChangeDetectionStrategy, Component } from "@angular/core";
+
+@Component({
+ selector: "app-vault-v3",
+ imports: [],
+ template: "Vault V3 Component
",
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class VaultComponent {}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index be2ea75203c..6010110f069 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -72,6 +72,9 @@ export enum FeatureFlag {
/* Innovation */
PM19148_InnovationArchive = "pm-19148-innovation-archive",
+ /* Desktop */
+ DesktopUiMigrationMilestone1 = "desktop-ui-migration-milestone-1",
+
/* UIF */
RouterFocusManagement = "router-focus-management",
}
@@ -152,6 +155,9 @@ export const DefaultFeatureFlagValue = {
/* Innovation */
[FeatureFlag.PM19148_InnovationArchive]: FALSE,
+ /* Desktop */
+ [FeatureFlag.DesktopUiMigrationMilestone1]: FALSE,
+
/* UIF */
[FeatureFlag.RouterFocusManagement]: FALSE,
} satisfies Record;