diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index bd2bd917a9d..79169a71dc9 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -559,6 +559,18 @@
"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"
},
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html
index 6e6e30b359b..86188915796 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html
@@ -35,5 +35,8 @@
{{ "assignToCollections" | i18n }}
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts
index 94b4c2b855b..c2bfc6b845f 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts
@@ -3,14 +3,17 @@
import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, Input, OnInit } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
-import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
+import { BehaviorSubject, firstValueFrom, map, of, switchMap } from "rxjs";
import { filter } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -72,6 +75,24 @@ export class ItemMoreOptionsComponent implements OnInit {
switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)),
);
+ /**
+ * Observable that emits a boolean value indicating if the user is authorized to archive the cipher.
+ * @protected
+ */
+ protected canArchive$ = this.configService
+ .getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive)
+ .pipe(
+ switchMap((enabled) => {
+ if (!enabled) {
+ return of(false);
+ }
+ return this._cipher$.pipe(
+ filter((c) => c != null),
+ map((c) => !c.isArchived && c.organizationId == null),
+ );
+ }),
+ );
+
/** Boolean dependent on the current user having access to an organization */
protected hasOrganizations = false;
@@ -86,6 +107,7 @@ export class ItemMoreOptionsComponent implements OnInit {
private accountService: AccountService,
private organizationService: OrganizationService,
private cipherAuthorizationService: CipherAuthorizationService,
+ private configService: ConfigService,
) {}
async ngOnInit(): Promise {
@@ -196,4 +218,23 @@ export class ItemMoreOptionsComponent implements OnInit {
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.cipherService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("itemSentToArchive"),
+ });
+ }
}
diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts
index 8114f51ab4c..ad463cea2d0 100644
--- a/apps/browser/src/vault/popup/settings/archive.component.ts
+++ b/apps/browser/src/vault/popup/settings/archive.component.ts
@@ -118,7 +118,14 @@ export class ArchiveComponent {
if (!(await this.canInteract(cipher))) {
return;
}
- // TODO: Implement once endpoint is available
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ await this.cipherService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
+
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("itemRemovedFromArchive"),
+ });
}
async clone(cipher: CipherView) {
diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts
index 1e4275ff89b..d5b3081c770 100644
--- a/libs/common/src/vault/abstractions/cipher.service.ts
+++ b/libs/common/src/vault/abstractions/cipher.service.ts
@@ -197,6 +197,8 @@ export abstract class CipherService implements UserKeyRotationDataProvider;
abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise;
abstract restoreManyWithServer(ids: string[], orgId?: string): Promise;
+ abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise;
+ abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise;
abstract getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise;
abstract setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId): Promise;
/**
diff --git a/libs/common/src/vault/models/request/cipher-bulk-archive.request.ts b/libs/common/src/vault/models/request/cipher-bulk-archive.request.ts
new file mode 100644
index 00000000000..a75ab632b3a
--- /dev/null
+++ b/libs/common/src/vault/models/request/cipher-bulk-archive.request.ts
@@ -0,0 +1,17 @@
+import { CipherId } from "@bitwarden/common/types/guid";
+
+export class CipherBulkArchiveRequest {
+ ids: CipherId[];
+
+ constructor(ids: CipherId[]) {
+ this.ids = ids == null ? [] : ids;
+ }
+}
+
+export class CipherBulkUnarchiveRequest {
+ ids: CipherId[];
+
+ constructor(ids: CipherId[]) {
+ this.ids = ids == null ? [] : ids;
+ }
+}
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index 0217ffe932c..08608f3121d 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -14,6 +14,10 @@ import {
} from "rxjs";
import { SemVer } from "semver";
+import {
+ CipherBulkArchiveRequest,
+ CipherBulkUnarchiveRequest,
+} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
import { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
@@ -1313,6 +1317,46 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore({ id: id, revisionDate: response.revisionDate }, userId);
}
+ async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise {
+ const request = new CipherBulkArchiveRequest(Array.isArray(ids) ? ids : [ids]);
+ const r = await this.apiService.send("PUT", "/ciphers/archive", request, true, true);
+ const response = new ListResponse(r, CipherResponse);
+
+ await this.updateEncryptedCipherState((ciphers) => {
+ for (const cipher of response.data) {
+ const localCipher = ciphers[cipher.id as CipherId];
+
+ if (localCipher == null) {
+ continue;
+ }
+
+ localCipher.archivedDate = cipher.archivedDate;
+ localCipher.revisionDate = cipher.revisionDate;
+ }
+ return ciphers;
+ }, userId);
+ }
+
+ async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise {
+ const request = new CipherBulkUnarchiveRequest(Array.isArray(ids) ? ids : [ids]);
+ const r = await this.apiService.send("PUT", "/ciphers/unarchive", request, true, true);
+ const response = new ListResponse(r, CipherResponse);
+
+ await this.updateEncryptedCipherState((ciphers) => {
+ for (const cipher of response.data) {
+ const localCipher = ciphers[cipher.id as CipherId];
+
+ if (localCipher == null) {
+ continue;
+ }
+
+ localCipher.archivedDate = cipher.archivedDate;
+ localCipher.revisionDate = cipher.revisionDate;
+ }
+ return ciphers;
+ }, userId);
+ }
+
/**
* No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
* The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore