1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +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

@@ -53,6 +53,9 @@ export enum FeatureFlag {
IpcChannelFramework = "ipc-channel-framework",
InactiveUserServerNotification = "pm-25130-receive-push-notifications-for-inactive-users",
PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked",
/* Innovation */
PM19148_InnovationArchive = "pm-19148-innovation-archive",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -112,6 +115,9 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.IpcChannelFramework]: FALSE,
[FeatureFlag.InactiveUserServerNotification]: FALSE,
[FeatureFlag.PushNotificationsWhenLocked]: FALSE,
/* Innovation */
[FeatureFlag.PM19148_InnovationArchive]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -36,6 +36,7 @@ export class CipherExport {
req.creationDate = null;
req.revisionDate = null;
req.deletedDate = null;
req.archivedDate = null;
return req;
}
@@ -84,6 +85,7 @@ export class CipherExport {
view.creationDate = req.creationDate ? new Date(req.creationDate) : view.creationDate;
view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : view.revisionDate;
view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : view.deletedDate;
view.archivedDate = req.archivedDate ? new Date(req.archivedDate) : view.archivedDate;
return view;
}
@@ -128,6 +130,7 @@ export class CipherExport {
domain.creationDate = req.creationDate ? new Date(req.creationDate) : null;
domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
domain.archivedDate = req.archivedDate ? new Date(req.archivedDate) : null;
return domain;
}
@@ -149,6 +152,7 @@ export class CipherExport {
revisionDate: Date = null;
creationDate: Date = null;
deletedDate: Date = null;
archivedDate: Date = null;
key: string;
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print
@@ -195,5 +199,6 @@ export class CipherExport {
this.creationDate = o.creationDate;
this.revisionDate = o.revisionDate;
this.deletedDate = o.deletedDate;
this.archivedDate = o.archivedDate;
}
}

View File

@@ -39,7 +39,8 @@ export class CipherData {
passwordHistory?: PasswordHistoryData[];
collectionIds?: string[];
creationDate: string;
deletedDate: string | null;
deletedDate: string | undefined;
archivedDate: string | undefined;
reprompt: CipherRepromptType;
key: string;
@@ -63,6 +64,7 @@ export class CipherData {
this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds;
this.creationDate = response.creationDate;
this.deletedDate = response.deletedDate;
this.archivedDate = response.archivedDate;
this.reprompt = response.reprompt;
this.key = response.key;

View File

@@ -60,13 +60,14 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
localData: null,
creationDate: null,
deletedDate: null,
deletedDate: undefined,
reprompt: undefined,
attachments: null,
fields: null,
passwordHistory: null,
key: null,
permissions: undefined,
archivedDate: undefined,
});
});
@@ -84,7 +85,7 @@ describe("Cipher DTO", () => {
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null;
cipher.deletedDate = undefined;
cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
cipher.permissions = new CipherPermissionsApi();
@@ -123,7 +124,7 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
localData: undefined,
permissions: new CipherPermissionsApi(),
@@ -149,10 +150,11 @@ describe("Cipher DTO", () => {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
permissions: new CipherPermissionsApi(),
reprompt: CipherRepromptType.None,
key: "EncryptedString",
archivedDate: undefined,
login: {
uris: [
{
@@ -224,10 +226,11 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
localData: null,
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
permissions: new CipherPermissionsApi(),
reprompt: 0,
key: { encryptedString: "EncryptedString", encryptionType: 0 },
archivedDate: undefined,
login: {
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
autofillOnPageLoad: false,
@@ -302,10 +305,11 @@ describe("Cipher DTO", () => {
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null;
cipher.deletedDate = undefined;
cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
cipher.permissions = new CipherPermissionsApi();
cipher.archivedDate = undefined;
const loginView = new LoginView();
loginView.username = "username";
@@ -347,10 +351,11 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
localData: undefined,
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
});
});
});
@@ -372,13 +377,14 @@ describe("Cipher DTO", () => {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
reprompt: CipherRepromptType.None,
key: "EncKey",
secureNote: {
type: SecureNoteType.Generic,
},
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
};
});
@@ -401,7 +407,7 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
localData: null,
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
secureNote: { type: SecureNoteType.Generic },
attachments: null,
@@ -409,6 +415,7 @@ describe("Cipher DTO", () => {
passwordHistory: null,
key: { encryptedString: "EncKey", encryptionType: 0 },
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
});
});
@@ -431,12 +438,13 @@ describe("Cipher DTO", () => {
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null;
cipher.deletedDate = undefined;
cipher.reprompt = CipherRepromptType.None;
cipher.secureNote = new SecureNote();
cipher.secureNote.type = SecureNoteType.Generic;
cipher.key = mockEnc("EncKey");
cipher.permissions = new CipherPermissionsApi();
cipher.archivedDate = undefined;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
@@ -470,10 +478,11 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
localData: undefined,
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
});
});
});
@@ -495,7 +504,7 @@ describe("Cipher DTO", () => {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
permissions: new CipherPermissionsApi(),
reprompt: CipherRepromptType.None,
card: {
@@ -507,6 +516,7 @@ describe("Cipher DTO", () => {
code: "EncryptedString",
},
key: "EncKey",
archivedDate: undefined,
};
});
@@ -529,7 +539,7 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
localData: null,
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
card: {
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
@@ -544,6 +554,7 @@ describe("Cipher DTO", () => {
passwordHistory: null,
key: { encryptedString: "EncKey", encryptionType: 0 },
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
});
});
@@ -566,10 +577,11 @@ describe("Cipher DTO", () => {
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null;
cipher.deletedDate = undefined;
cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
cipher.permissions = new CipherPermissionsApi();
cipher.archivedDate = undefined;
const cardView = new CardView();
cardView.cardholderName = "cardholderName";
@@ -611,10 +623,11 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
localData: undefined,
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
});
});
});
@@ -636,10 +649,11 @@ describe("Cipher DTO", () => {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
permissions: new CipherPermissionsApi(),
reprompt: CipherRepromptType.None,
key: "EncKey",
archivedDate: undefined,
identity: {
title: "EncryptedString",
firstName: "EncryptedString",
@@ -682,8 +696,9 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
localData: null,
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
archivedDate: undefined,
identity: {
title: { encryptedString: "EncryptedString", encryptionType: 0 },
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
@@ -731,10 +746,11 @@ describe("Cipher DTO", () => {
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null;
cipher.deletedDate = undefined;
cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
cipher.permissions = new CipherPermissionsApi();
cipher.archivedDate = undefined;
const identityView = new IdentityView();
identityView.firstName = "firstName";
@@ -776,10 +792,11 @@ describe("Cipher DTO", () => {
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null,
deletedDate: undefined,
reprompt: 0,
localData: undefined,
permissions: new CipherPermissionsApi(),
archivedDate: undefined,
});
});
});
@@ -793,6 +810,7 @@ describe("Cipher DTO", () => {
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
const deletedDate = new Date("2022-09-04T01:06:40.441Z");
const archivedDate = new Date("2022-10-04T01:06:40.441Z");
const actual = Cipher.fromJSON({
name: "myName",
notes: "myNotes",
@@ -801,6 +819,7 @@ describe("Cipher DTO", () => {
fields: ["field1", "field2"] as any,
passwordHistory: ["ph1", "ph2"] as any,
deletedDate: deletedDate.toISOString(),
archivedDate: archivedDate.toISOString(),
} as Jsonify<Cipher>);
expect(actual).toMatchObject({
@@ -811,6 +830,7 @@ describe("Cipher DTO", () => {
fields: ["field1_fromJSON", "field2_fromJSON"],
passwordHistory: ["ph1_fromJSON", "ph2_fromJSON"],
deletedDate: deletedDate,
archivedDate: archivedDate,
});
expect(actual).toBeInstanceOf(Cipher);
});
@@ -862,7 +882,8 @@ describe("Cipher DTO", () => {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
archivedDate: undefined,
reprompt: CipherRepromptType.None,
key: "EncryptedString",
login: {
@@ -1084,6 +1105,7 @@ describe("Cipher DTO", () => {
],
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: undefined,
archivedDate: undefined,
revisionDate: "2022-01-31T12:00:00.000Z",
};
@@ -1105,7 +1127,8 @@ describe("Cipher DTO", () => {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
archivedDate: undefined,
reprompt: CipherRepromptType.None,
key: "EncryptedString",
login: {

View File

@@ -56,7 +56,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
passwordHistory: Password[];
collectionIds: string[];
creationDate: Date;
deletedDate: Date;
deletedDate: Date | undefined;
archivedDate: Date | undefined;
reprompt: CipherRepromptType;
key: EncString;
@@ -94,7 +95,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
this.collectionIds = obj.collectionIds;
this.localData = localData;
this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null;
this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : null;
this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : undefined;
this.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : undefined;
this.reprompt = obj.reprompt;
switch (this.type) {
@@ -244,10 +246,11 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
c.type = this.type;
c.collectionIds = this.collectionIds;
c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null;
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : undefined;
c.reprompt = this.reprompt;
c.key = this.key?.encryptedString;
c.permissions = this.permissions;
c.archivedDate = this.archivedDate != null ? this.archivedDate.toISOString() : undefined;
this.buildDataModel(this, c, {
name: null,
@@ -296,11 +299,12 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
const notes = EncString.fromJSON(obj.notes);
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
const deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a));
const fields = obj.fields?.map((f: any) => Field.fromJSON(f));
const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph));
const key = EncString.fromJSON(obj.key);
const archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
Object.assign(domain, obj, {
name,
@@ -312,6 +316,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
fields,
passwordHistory,
key,
archivedDate,
});
switch (obj.type) {
@@ -369,6 +374,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
revisionDate: this.revisionDate?.toISOString(),
creationDate: this.creationDate?.toISOString(),
deletedDate: this.deletedDate?.toISOString(),
archivedDate: this.archivedDate?.toISOString(),
reprompt: this.reprompt,
// Initialize all cipher-type-specific properties as undefined
login: undefined,
@@ -434,7 +440,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? [];
cipher.creationDate = new Date(sdkCipher.creationDate);
cipher.revisionDate = new Date(sdkCipher.revisionDate);
cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : null;
cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : undefined;
cipher.archivedDate = sdkCipher.archivedDate ? new Date(sdkCipher.archivedDate) : undefined;
cipher.reprompt = sdkCipher.reprompt;
// Cipher type specific properties

View File

@@ -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;
}
}

View File

@@ -35,6 +35,7 @@ export class CipherRequest {
attachments: { [id: string]: string };
attachments2: { [id: string]: AttachmentRequest };
lastKnownRevisionDate: Date;
archivedDate: Date | null;
reprompt: CipherRepromptType;
key: string;
@@ -47,6 +48,7 @@ export class CipherRequest {
this.notes = cipher.notes ? cipher.notes.encryptedString : null;
this.favorite = cipher.favorite;
this.lastKnownRevisionDate = cipher.revisionDate;
this.archivedDate = cipher.archivedDate;
this.reprompt = cipher.reprompt;
this.key = cipher.key?.encryptedString;

View File

@@ -38,6 +38,7 @@ export class CipherResponse extends BaseResponse {
collectionIds: string[];
creationDate: string;
deletedDate: string;
archivedDate: string;
reprompt: CipherRepromptType;
key: string;
@@ -62,6 +63,7 @@ export class CipherResponse extends BaseResponse {
this.collectionIds = this.getResponseProperty("CollectionIds");
this.creationDate = this.getResponseProperty("CreationDate");
this.deletedDate = this.getResponseProperty("DeletedDate");
this.archivedDate = this.getResponseProperty("ArchivedDate");
const login = this.getResponseProperty("Login");
if (login != null) {

View File

@@ -163,6 +163,7 @@ describe("CipherView", () => {
creationDate: "2022-01-01T12:00:00.000Z",
revisionDate: "2022-01-02T12:00:00.000Z",
deletedDate: undefined,
archivedDate: undefined,
};
});

View File

@@ -49,7 +49,8 @@ export class CipherView implements View, InitializerMetadata {
collectionIds: string[] = null;
revisionDate: Date = null;
creationDate: Date = null;
deletedDate: Date = null;
deletedDate: Date | null = null;
archivedDate: Date | null = null;
reprompt: CipherRepromptType = CipherRepromptType.None;
// We need a copy of the encrypted key so we can pass it to
// the SdkCipherView during encryption
@@ -79,6 +80,7 @@ export class CipherView implements View, InitializerMetadata {
this.revisionDate = c.revisionDate;
this.creationDate = c.creationDate;
this.deletedDate = c.deletedDate;
this.archivedDate = c.archivedDate;
// Old locally stored ciphers might have reprompt == null. If so set it to None.
this.reprompt = c.reprompt ?? CipherRepromptType.None;
this.key = c.key;
@@ -143,6 +145,10 @@ export class CipherView implements View, InitializerMetadata {
return this.deletedDate != null;
}
get isArchived(): boolean {
return this.archivedDate != null;
}
get linkedFieldOptions() {
return this.item?.linkedFieldOptions;
}
@@ -197,6 +203,7 @@ export class CipherView implements View, InitializerMetadata {
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
@@ -217,6 +224,7 @@ export class CipherView implements View, InitializerMetadata {
creationDate: creationDate,
revisionDate: revisionDate,
deletedDate: deletedDate,
archivedDate: archivedDate,
attachments: attachments,
fields: fields,
passwordHistory: passwordHistory,
@@ -277,6 +285,7 @@ export class CipherView implements View, InitializerMetadata {
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
cipherView.archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
cipherView.key = EncString.fromJSON(obj.key);
@@ -330,6 +339,7 @@ export class CipherView implements View, InitializerMetadata {
revisionDate: (this.revisionDate ?? new Date()).toISOString(),
creationDate: (this.creationDate ?? new Date()).toISOString(),
deletedDate: this.deletedDate?.toISOString(),
archivedDate: this.archivedDate?.toISOString(),
reprompt: this.reprompt ?? CipherRepromptType.None,
key: this.key?.toSdk(),
// Cipher type specific properties are set in the switch statement below

View File

@@ -68,6 +68,7 @@ const cipherData: CipherData = {
deletedDate: null,
permissions: new CipherPermissionsApi(),
key: "EncKey",
archivedDate: null,
reprompt: CipherRepromptType.None,
login: {
uris: [

View File

@@ -286,6 +286,7 @@ export class CipherService implements CipherServiceAbstraction {
cipher.collectionIds = model.collectionIds;
cipher.creationDate = model.creationDate;
cipher.revisionDate = model.revisionDate;
cipher.archivedDate = model.archivedDate;
cipher.reprompt = model.reprompt;
cipher.edit = model.edit;
@@ -634,6 +635,10 @@ export class CipherService implements CipherServiceAbstraction {
);
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
const archiveFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM19148_InnovationArchive,
);
return ciphers.filter((cipher) => {
const type = CipherViewLikeUtils.getType(cipher);
const login = CipherViewLikeUtils.getLogin(cipher);
@@ -643,6 +648,10 @@ export class CipherService implements CipherServiceAbstraction {
return false;
}
if (archiveFeatureEnabled && CipherViewLikeUtils.isArchived(cipher)) {
return false;
}
if (Array.isArray(includeOtherTypes) && includeOtherTypes.includes(type) && !cipherIsLogin) {
return true;
}
@@ -666,8 +675,16 @@ export class CipherService implements CipherServiceAbstraction {
userId: UserId,
): Promise<CipherView[]> {
const ciphers = await this.getAllDecrypted(userId);
const archiveFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM19148_InnovationArchive,
);
return ciphers
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
.filter(
(cipher) =>
cipher.deletedDate == null &&
(!archiveFeatureEnabled || !cipher.isArchived) &&
type.includes(cipher.type),
)
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
}

View File

@@ -48,7 +48,8 @@ const cipherData: CipherData = {
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
deletedDate: undefined,
archivedDate: undefined,
permissions: new CipherPermissionsApi(),
key: "EncKey",
reprompt: CipherRepromptType.None,

View File

@@ -110,6 +110,32 @@ describe("CipherViewLikeUtils", () => {
});
});
describe("isArchived", () => {
it("returns true when the cipher is archived", () => {
const cipherListView = {
id: "1",
archivedDate: "2024-02-02",
type: "identity",
} as unknown as CipherListView;
const cipherView = createCipherView();
cipherView.archivedDate = new Date();
expect(CipherViewLikeUtils.isArchived(cipherListView)).toBe(true);
expect(CipherViewLikeUtils.isArchived(cipherView)).toBe(true);
});
it("returns false when the cipher is not archived", () => {
const cipherListView = {
id: "2",
type: "identity",
} as unknown as CipherListView;
const cipherView = createCipherView();
expect(CipherViewLikeUtils.isArchived(cipherListView)).toBe(false);
expect(CipherViewLikeUtils.isArchived(cipherView)).toBe(false);
});
});
describe("isDeleted", () => {
it("returns true when the cipher is deleted", () => {
const cipherListView = { deletedDate: "2024-02-02", type: "identity" } as CipherListView;

View File

@@ -71,6 +71,15 @@ export class CipherViewLikeUtils {
return cipher.type === CipherType.Card ? cipher.card : null;
};
/** @returns `true` when the cipher has been archived, `false` otherwise. */
static isArchived = (cipher: CipherViewLike): boolean => {
if (this.isCipherListView(cipher)) {
return !!cipher.archivedDate;
}
return cipher.isArchived;
};
/** @returns `true` when the cipher has been deleted, `false` otherwise. */
static isDeleted = (cipher: CipherViewLike): boolean => {
if (this.isCipherListView(cipher)) {