1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[PM-24533] Initialize Archive Feature (#16226)

* [PM-19237] Add Archive Filter Type (#13852)
* Browser can archive and unarchive items
* Create Archive Cipher Service
* Add flag and premium permissions to Archive 

---------

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Shane <smelton@bitwarden.com>
Co-authored-by: Patrick Pimentel <ppimentel@bitwarden.com>
This commit is contained in:
Jason Ng
2025-09-22 11:06:02 -04:00
committed by GitHub
parent 04881556df
commit dbec02cf8d
48 changed files with 1166 additions and 62 deletions

View File

@@ -550,6 +550,33 @@
"resetSearch": {
"message": "Reset search"
},
"archive": {
"message": "Archive"
},
"unarchive": {
"message": "Unarchive"
},
"itemsInArchive": {
"message": "Items in archive"
},
"noItemsInArchive": {
"message": "No items in archive"
},
"noItemsInArchiveDesc": {
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
},
"itemSentToArchive": {
"message": "Item sent to archive"
},
"itemRemovedFromArchive": {
"message": "Item removed from archive"
},
"archiveItem": {
"message": "Archive item"
},
"archiveItemConfirmDesc": {
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
},
"edit": {
"message": "Edit"
},

View File

@@ -30,6 +30,7 @@ import {
LoginDecryptionOptionsComponent,
LoginSecondaryContentComponent,
LoginViaAuthRequestComponent,
NewDeviceVerificationComponent,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationStartComponent,
@@ -38,7 +39,6 @@ import {
SsoComponent,
TwoFactorAuthComponent,
TwoFactorAuthGuard,
NewDeviceVerificationComponent,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
@@ -80,6 +80,7 @@ import { canAccessAtRiskPasswords } from "../vault/popup/guards/at-risk-password
import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard";
import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { ArchiveComponent } from "../vault/popup/settings/archive.component";
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
@@ -675,6 +676,12 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
{
path: "archive",
component: ArchiveComponent,
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
{
path: "security",
component: AnonLayoutWrapperComponent,

View File

@@ -145,6 +145,8 @@ import {
DefaultSshImportPromptService,
PasswordRepromptService,
SshImportPromptService,
CipherArchiveService,
DefaultCipherArchiveService,
} from "@bitwarden/vault";
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
@@ -703,6 +705,18 @@ const safeProviders: SafeProvider[] = [
useClass: ExtensionDeviceManagementComponentService,
deps: [],
}),
safeProvider({
provide: CipherArchiveService,
useClass: DefaultCipherArchiveService,
deps: [
CipherService,
ApiService,
DialogService,
PasswordRepromptService,
BillingAccountProfileStateService,
ConfigService,
],
}),
];
@NgModule({

View File

@@ -38,5 +38,10 @@
{{ "assignToCollections" | i18n }}
</a>
</ng-container>
@if (canArchive$ | async) {
<button type="button" bitMenuItem (click)="archive()" *ngIf="canArchive$ | async">
{{ "archive" | i18n }}
</button>
}
</bit-menu>
</bit-item-action>

View File

@@ -3,7 +3,8 @@
import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, Input } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
import { BehaviorSubject, combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
import { filter } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -11,6 +12,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
@@ -26,7 +28,7 @@ import {
MenuModule,
ToastService,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherArchiveService, PasswordRepromptService } from "@bitwarden/vault";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
@@ -103,6 +105,20 @@ export class ItemMoreOptionsComponent {
}),
);
/** Observable Boolean checking if item can show Archive menu option */
protected canArchive$ = combineLatest([
this._cipher$,
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
),
]).pipe(
filter(([cipher, userId]) => cipher != null && userId != null),
map(([cipher, canArchive]) => {
return canArchive && !CipherViewLikeUtils.isArchived(cipher) && cipher.organizationId == null;
}),
);
constructor(
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService,
@@ -116,6 +132,7 @@ export class ItemMoreOptionsComponent {
private cipherAuthorizationService: CipherAuthorizationService,
private collectionService: CollectionService,
private restrictedItemTypesService: RestrictedItemTypesService,
private cipherArchiveService: CipherArchiveService,
) {}
get canEdit() {
@@ -233,4 +250,23 @@ export class ItemMoreOptionsComponent {
queryParams: { cipherId: this.cipher.id },
});
}
async archive() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "archiveItem" },
content: { key: "archiveItemConfirmDesc" },
type: "info",
});
if (!confirmed) {
return;
}
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("itemSentToArchive"),
});
}
}

View File

@@ -1,4 +1,4 @@
import { WritableSignal, signal } from "@angular/core";
import { signal, WritableSignal } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
@@ -8,10 +8,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec";
import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
@@ -25,6 +26,7 @@ import {
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { CipherArchiveService } from "@bitwarden/vault";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
@@ -43,7 +45,7 @@ describe("VaultPopupItemsService", () => {
let mockOrg: Organization;
let mockCollections: CollectionView[];
let activeUserLastSync$: BehaviorSubject<Date>;
let activeUserLastSync$: BehaviorSubject<Date | null>;
let viewCacheService: {
signal: jest.Mock;
mockSignal: WritableSignal<string | null>;
@@ -64,6 +66,9 @@ describe("VaultPopupItemsService", () => {
const inlineMenuFieldQualificationServiceMock = mock<InlineMenuFieldQualificationService>();
const userId = Utils.newGuid() as UserId;
const accountServiceMock = mockAccountServiceWith(userId);
const configServiceMock = mock<ConfigService>();
const cipherArchiveServiceMock = mock<CipherArchiveService>();
cipherArchiveServiceMock.userCanArchive$.mockReturnValue(of(true));
const restrictedItemTypesService = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
@@ -101,7 +106,7 @@ describe("VaultPopupItemsService", () => {
failedToDecryptCiphersSubject.asObservable(),
);
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers);
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers!);
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
ciphers.filter((c) => ["0", "1"].includes(uuidAsString(c.id))),
);
@@ -142,8 +147,9 @@ describe("VaultPopupItemsService", () => {
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections));
activeUserLastSync$ = new BehaviorSubject(new Date());
activeUserLastSync$ = new BehaviorSubject<Date | null>(new Date());
syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$);
configServiceMock.getFeatureFlag$.mockReturnValue(of(true));
const testSearchSignal = createMockSignal<string | null>("");
viewCacheService = {
@@ -168,10 +174,15 @@ describe("VaultPopupItemsService", () => {
useValue: inlineMenuFieldQualificationServiceMock,
},
{ provide: PopupViewCacheService, useValue: viewCacheService },
{ provide: ConfigService, useValue: configServiceMock },
{
provide: RestrictedItemTypesService,
useValue: restrictedItemTypesService,
},
{
provide: CipherArchiveService,
useValue: cipherArchiveServiceMock,
},
],
});
@@ -297,7 +308,7 @@ describe("VaultPopupItemsService", () => {
const searchText = "Login";
searchService.searchCiphers.mockImplementation(async (userId, q, _, ciphers) => {
return ciphers.filter((cipher) => {
return ciphers!.filter((cipher) => {
return cipher.name.includes(searchText);
});
});
@@ -390,12 +401,12 @@ describe("VaultPopupItemsService", () => {
});
});
it("should return true when all ciphers are deleted", (done) => {
it("should return true when all ciphers are deleted/archived", (done) => {
cipherServiceMock.cipherListViews$.mockReturnValue(
of([
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: false, isArchived: true },
] as CipherView[]),
);

View File

@@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -34,6 +35,7 @@ import {
CipherViewLike,
CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { CipherArchiveService } from "@bitwarden/vault";
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
@@ -133,14 +135,24 @@ export class VaultPopupItemsService {
shareReplay({ refCount: true, bufferSize: 1 }),
);
private userCanArchive$ = this.activeUserId$.pipe(
switchMap((userId) => {
return this.cipherArchiveService.userCanArchive$(userId);
}),
);
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
switchMap((ciphers) =>
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
map(([organizations, collections]) => {
combineLatest([this.organizations$, this.decryptedCollections$, this.userCanArchive$]).pipe(
map(([organizations, collections, canArchive]) => {
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
return ciphers
.filter((c) => !CipherViewLikeUtils.isDeleted(c))
.filter(
(c) =>
!CipherViewLikeUtils.isDeleted(c) &&
(!canArchive || !CipherViewLikeUtils.isArchived(c)),
)
.map((cipher) => {
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
(colId) => collectionMap[colId as CollectionId],
@@ -330,6 +342,8 @@ export class VaultPopupItemsService {
private accountService: AccountService,
private ngZone: NgZone,
private restrictedItemTypesService: RestrictedItemTypesService,
private configService: ConfigService,
private cipherArchiveService: CipherArchiveService,
) {}
applyFilter(newSearchText: string) {

View File

@@ -0,0 +1,81 @@
<popup-page [loading]="loading$ | async">
<popup-header slot="header" [pageTitle]="'archive' | i18n" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
@if (archivedCiphers$ | async; as archivedItems) {
@if (archivedItems.length) {
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">
{{ "itemsInArchive" | i18n }}
</h2>
<span bitTypography="body1" slot="end">{{ archivedItems.length }}</span>
</bit-section-header>
<bit-item-group>
@for (cipher of archivedItems; track cipher.id) {
<bit-item>
<button
bit-item-content
type="button"
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
(click)="view(cipher)"
>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
@if (cipher.hasAttachments) {
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
}
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>
<bit-item-action slot="end">
<button
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
label="{{ 'options' | i18n }}"
[appA11yTitle]="'moreOptionsTitle' | i18n: cipher.name"
[bitMenuTriggerFor]="moreOptions"
></button>
<bit-menu #moreOptions>
<button type="button" bitMenuItem (click)="edit(cipher)">
{{ "edit" | i18n }}
</button>
<button type="button" bitMenuItem (click)="clone(cipher)">
{{ "clone" | i18n }}
</button>
<button type="button" bitMenuItem (click)="unarchive(cipher)">
{{ "unarchive" | i18n }}
</button>
<button
type="button"
bitMenuItem
*appCanDeleteCipher="cipher"
(click)="delete(cipher)"
>
<span class="tw-text-danger">
{{ "delete" | i18n }}
</span>
</button>
</bit-menu>
</bit-item-action>
</bit-item>
}
</bit-item-group>
</bit-section>
} @else {
<bit-no-items class="tw-flex tw-h-full tw-items-center tw-justify-center">
<ng-container slot="title">
{{ "noItemsInArchive" | i18n }}
</ng-container>
<ng-container slot="description">
{{ "noItemsInArchiveDesc" | i18n }}
</ng-container>
</bit-no-items>
}
}
</popup-page>

View File

@@ -0,0 +1,159 @@
import { CommonModule } from "@angular/common";
import { Component, inject } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map, Observable, startWith, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
DialogService,
IconButtonModule,
ItemModule,
MenuModule,
NoItemsModule,
SectionComponent,
SectionHeaderComponent,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { CanDeleteCipherDirective, CipherArchiveService } from "@bitwarden/vault";
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";
@Component({
templateUrl: "archive.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
NoItemsModule,
ItemModule,
MenuModule,
IconButtonModule,
CanDeleteCipherDirective,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
],
})
export class ArchiveComponent {
private dialogService = inject(DialogService);
private router = inject(Router);
private cipherService = inject(CipherService);
private accountService = inject(AccountService);
private logService = inject(LogService);
private toastService = inject(ToastService);
private i18nService = inject(I18nService);
private cipherArchiveService = inject(CipherArchiveService);
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
protected archivedCiphers$ = this.userId$.pipe(
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
);
protected loading$ = this.archivedCiphers$.pipe(
map(() => false),
startWith(true),
);
async view(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) {
return;
}
await this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id, type: cipher.type },
});
}
async edit(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) {
return;
}
await this.router.navigate(["/edit-cipher"], {
queryParams: { cipherId: cipher.id, type: cipher.type },
});
}
async delete(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },
content: { key: "deleteItemConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
const activeUserId = await firstValueFrom(this.userId$);
try {
await this.cipherService.softDeleteWithServer(cipher.id, activeUserId);
} catch (e) {
this.logService.error(e);
return;
}
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("deletedItem"),
});
}
async unarchive(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) {
return;
}
const activeUserId = await firstValueFrom(this.userId$);
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("itemRemovedFromArchive"),
});
}
async clone(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) {
return;
}
if (cipher.login?.hasFido2Credentials) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "passkeyNotCopied" },
content: { key: "passkeyNotCopiedAlert" },
type: "info",
});
if (!confirmed) {
return;
}
}
await this.router.navigate(["/clone-cipher"], {
queryParams: {
clone: true.toString(),
cipherId: cipher.id,
type: cipher.type,
},
});
}
}

View File

@@ -34,6 +34,14 @@
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
@if (userCanArchive() || showArchiveFilter()) {
<bit-item>
<a bit-item-content routerLink="/archive">
{{ "archive" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
}
<bit-item>
<a bit-item-content routerLink="/trash">
{{ "trash" | i18n }}

View File

@@ -1,5 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { Router, RouterModule } from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
@@ -10,6 +11,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
import { CipherArchiveService } from "@bitwarden/vault";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
@@ -32,6 +34,17 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
})
export class VaultSettingsV2Component implements OnInit, OnDestroy {
lastSync = "--";
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
// Check if user is premium user, they will be able to archive items
protected userCanArchive = toSignal(
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
);
// Check if user has archived items (does not check if user is premium)
protected showArchiveFilter = toSignal(
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
);
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
getUserId,
@@ -47,6 +60,7 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
private i18nService: I18nService,
private nudgeService: NudgesService,
private accountService: AccountService,
private cipherArchiveService: CipherArchiveService,
) {}
async ngOnInit() {