diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html
index c042af8cbac..407015d3a06 100644
--- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html
+++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html
@@ -37,14 +37,19 @@
@if (showArchiveItem()) {
@if (userCanArchive()) {
-
+
{{ "archiveNoun" | i18n }}
} @else {
-
+
{{ "archiveNoun" | i18n }}
@if (!userHasArchivedItems()) {
diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts
new file mode 100644
index 00000000000..fc30a3f8899
--- /dev/null
+++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts
@@ -0,0 +1,199 @@
+import { ChangeDetectionStrategy, Component, DebugElement, input } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+import { provideRouter, Router } from "@angular/router";
+import { mock } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { NudgesService } from "@bitwarden/angular/vault";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
+import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { DialogService, ToastService } from "@bitwarden/components";
+
+import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
+import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
+
+import { VaultSettingsV2Component } from "./vault-settings-v2.component";
+
+@Component({
+ selector: "popup-header",
+ template: ``,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+class MockPopupHeaderComponent {
+ readonly pageTitle = input();
+ readonly showBackButton = input();
+}
+
+@Component({
+ selector: "popup-page",
+ template: ``,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+class MockPopupPageComponent {}
+
+@Component({
+ selector: "app-pop-out",
+ template: ``,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+class MockPopOutComponent {
+ readonly show = input(true);
+}
+
+describe("VaultSettingsV2Component", () => {
+ let component: VaultSettingsV2Component;
+ let fixture: ComponentFixture;
+ let router: Router;
+ let mockCipherArchiveService: jest.Mocked;
+
+ const mockActiveAccount$ = new BehaviorSubject<{ id: string }>({
+ id: "user-id",
+ });
+ const mockUserCanArchive$ = new BehaviorSubject(false);
+ const mockHasArchiveFlagEnabled$ = new BehaviorSubject(true);
+ const mockArchivedCiphers$ = new BehaviorSubject([]);
+ const mockShowNudgeBadge$ = new BehaviorSubject(false);
+
+ const queryByTestId = (testId: string): DebugElement | null => {
+ return fixture.debugElement.query(By.css(`[data-test-id="${testId}"]`));
+ };
+
+ const setArchiveState = (
+ canArchive: boolean,
+ archivedItems: CipherView[] = [],
+ flagEnabled = true,
+ ) => {
+ mockUserCanArchive$.next(canArchive);
+ mockArchivedCiphers$.next(archivedItems);
+ mockHasArchiveFlagEnabled$.next(flagEnabled);
+ fixture.detectChanges();
+ };
+
+ beforeEach(async () => {
+ mockCipherArchiveService = mock({
+ userCanArchive$: jest.fn().mockReturnValue(mockUserCanArchive$),
+ hasArchiveFlagEnabled$: jest.fn().mockReturnValue(mockHasArchiveFlagEnabled$),
+ archivedCiphers$: jest.fn().mockReturnValue(mockArchivedCiphers$),
+ });
+
+ await TestBed.configureTestingModule({
+ imports: [VaultSettingsV2Component],
+ providers: [
+ provideRouter([
+ { path: "archive", component: VaultSettingsV2Component },
+ { path: "premium", component: VaultSettingsV2Component },
+ ]),
+ { provide: SyncService, useValue: mock() },
+ { provide: ToastService, useValue: mock() },
+ { provide: ConfigService, useValue: mock() },
+ { provide: DialogService, useValue: mock() },
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ { provide: CipherArchiveService, useValue: mockCipherArchiveService },
+ {
+ provide: NudgesService,
+ useValue: { showNudgeBadge$: jest.fn().mockReturnValue(mockShowNudgeBadge$) },
+ },
+
+ {
+ provide: BillingAccountProfileStateService,
+ useValue: mock(),
+ },
+ {
+ provide: AccountService,
+ useValue: { activeAccount$: mockActiveAccount$ },
+ },
+ ],
+ })
+ .overrideComponent(VaultSettingsV2Component, {
+ remove: {
+ imports: [PopupHeaderComponent, PopupPageComponent, PopOutComponent],
+ },
+ add: {
+ imports: [MockPopupHeaderComponent, MockPopupPageComponent, MockPopOutComponent],
+ },
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(VaultSettingsV2Component);
+ component = fixture.componentInstance;
+ router = TestBed.inject(Router);
+ jest.spyOn(router, "navigate");
+ });
+
+ describe("archive link", () => {
+ it("shows direct archive link when user can archive", () => {
+ setArchiveState(true);
+
+ const archiveLink = queryByTestId("archive-link");
+
+ expect(archiveLink.nativeElement.getAttribute("routerLink")).toBe("/archive");
+ });
+
+ it("routes to archive when user has archived items but cannot archive", async () => {
+ setArchiveState(false, [{ id: "cipher1" } as CipherView]);
+
+ const premiumArchiveLink = queryByTestId("premium-archive-link");
+
+ premiumArchiveLink.nativeElement.click();
+ await fixture.whenStable();
+
+ expect(router.navigate).toHaveBeenCalledWith(["/archive"]);
+ });
+
+ it("prompts for premium when user cannot archive and has no archived items", async () => {
+ setArchiveState(false, []);
+ const badge = component["premiumBadgeComponent"]();
+ jest.spyOn(badge, "promptForPremium");
+
+ const premiumArchiveLink = queryByTestId("premium-archive-link");
+
+ premiumArchiveLink.nativeElement.click();
+ await fixture.whenStable();
+
+ expect(badge.promptForPremium).toHaveBeenCalled();
+ });
+ });
+
+ describe("archive visibility", () => {
+ it("displays archive link when user can archive", () => {
+ setArchiveState(true);
+
+ const archiveLink = queryByTestId("archive-link");
+
+ expect(archiveLink).toBeTruthy();
+ expect(component["userCanArchive"]()).toBe(true);
+ });
+
+ it("hides archive link when feature flag is disabled", () => {
+ setArchiveState(false, [], false);
+
+ const archiveLink = queryByTestId("archive-link");
+ const premiumArchiveLink = queryByTestId("premium-archive-link");
+
+ expect(archiveLink).toBeNull();
+ expect(premiumArchiveLink).toBeNull();
+ expect(component["showArchiveItem"]()).toBe(false);
+ });
+
+ it("shows premium badge when user has no archived items and cannot archive", () => {
+ setArchiveState(false, []);
+
+ expect(component["premiumBadgeComponent"]()).toBeTruthy();
+ expect(component["userHasArchivedItems"]()).toBe(false);
+ });
+
+ it("hides premium badge when user has archived items", () => {
+ setArchiveState(false, [{ id: "cipher1" } as CipherView]);
+
+ expect(component["premiumBadgeComponent"]()).toBeUndefined();
+ expect(component["userHasArchivedItems"]()).toBe(true);
+ });
+ });
+});
diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts
index e085cb21c2d..c1d90d678cb 100644
--- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts
+++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts
@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
-import { Component, OnDestroy, OnInit } from "@angular/core";
+import { Component, OnDestroy, OnInit, viewChild } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { Router, RouterModule } from "@angular/router";
import { firstValueFrom, map, switchMap } from "rxjs";
@@ -42,6 +42,8 @@ import { BrowserPremiumUpgradePromptService } from "../services/browser-premium-
],
})
export class VaultSettingsV2Component implements OnInit, OnDestroy {
+ private readonly premiumBadgeComponent = viewChild(PremiumBadgeComponent);
+
lastSync = "--";
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
@@ -117,4 +119,18 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
this.lastSync = this.i18nService.t("never");
}
}
+
+ /**
+ * When a user can archive or has previously archived items, route them to
+ * the archive page. Otherwise, prompt them to upgrade to premium.
+ */
+ async conditionallyRouteToArchive(event: Event) {
+ event.preventDefault();
+ const premiumBadge = this.premiumBadgeComponent();
+ if (this.userCanArchive() || this.userHasArchivedItems()) {
+ await this.router.navigate(["/archive"]);
+ } else if (premiumBadge) {
+ await premiumBadge.promptForPremium(event);
+ }
+ }
}