diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts
index ec35fc7c554..71846cc6444 100644
--- a/apps/browser/src/popup/app.module.ts
+++ b/apps/browser/src/popup/app.module.ts
@@ -92,7 +92,7 @@ import "../platform/popup/locales";
TabsV2Component,
RemovePasswordComponent,
],
- exports: [],
+ exports: [CalloutModule],
providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent],
})
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
index 2e2ee5cd56b..e24db60a55a 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
@@ -10,7 +10,7 @@ import {
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
import { CopyableCipherFields } from "@bitwarden/sdk-internal";
-import { CopyAction, CopyCipherFieldDirective } from "@bitwarden/vault";
+import { CopyFieldAction, CopyCipherFieldDirective } from "@bitwarden/vault";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
@@ -18,7 +18,7 @@ type CipherItem = {
/** Translation key for the respective value */
key: string;
/** Property key on `CipherView` to retrieve the copy value */
- field: CopyAction;
+ field: CopyFieldAction;
};
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
@@ -48,7 +48,7 @@ export class ItemCopyActionsComponent {
* singleCopyableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
* code to be copied correctly. See #14167
*/
- get singleCopyableLogin() {
+ get singleCopyableLogin(): CipherItem | null {
const loginItems: CipherItem[] = [
{ key: "copyUsername", field: "username" },
{ key: "copyPassword", field: "password" },
@@ -62,7 +62,7 @@ export class ItemCopyActionsComponent {
) {
return {
key: this.i18nService.t("copyUsername"),
- field: "username",
+ field: "username" as const,
};
}
return this.findSingleCopyableItem(loginItems);
diff --git a/apps/browser/src/vault/popup/settings/archive.component.html b/apps/browser/src/vault/popup/settings/archive.component.html
index faaf0243fc7..059d636c60d 100644
--- a/apps/browser/src/vault/popup/settings/archive.component.html
+++ b/apps/browser/src/vault/popup/settings/archive.component.html
@@ -27,10 +27,10 @@
{{ cipher.name }}
- @if (cipher.hasAttachments) {
+ @if (CipherViewLikeUtils.hasAttachments(cipher)) {
}
- {{ cipher.subTitle }}
+ {{ CipherViewLikeUtils.subtitle(cipher) }}
@@ -45,7 +45,7 @@
type="button"
bitMenuItem
(click)="restore(cipher)"
- *ngIf="!cipher.decryptionFailure"
+ *ngIf="!hasDecryptionFailure(cipher)"
>
{{ "restore" | i18n }}
diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts
index 70ba6842a0d..bad6011b2d8 100644
--- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts
+++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts
@@ -12,7 +12,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId } 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,
@@ -85,10 +84,40 @@ export class TrashListItemsContainerComponent {
return collections[0]?.name;
}
- async restore(cipher: CipherView) {
+ /**
+ * Check if a cipher has attachments. CipherView has a hasAttachments getter,
+ * while CipherListView has an attachments count property.
+ */
+ hasAttachments(cipher: PopupCipherViewLike): boolean {
+ if ("hasAttachments" in cipher) {
+ return cipher.hasAttachments;
+ }
+ return cipher.attachments > 0;
+ }
+
+ /**
+ * Get the subtitle for a cipher. CipherView has a subTitle getter,
+ * while CipherListView has a subtitle property.
+ */
+ getSubtitle(cipher: PopupCipherViewLike): string | undefined {
+ if ("subTitle" in cipher) {
+ return cipher.subTitle;
+ }
+ return cipher.subtitle;
+ }
+
+ /**
+ * Check if a cipher has a decryption failure. CipherView has this property,
+ * while CipherListView does not.
+ */
+ hasDecryptionFailure(cipher: PopupCipherViewLike): boolean {
+ return "decryptionFailure" in cipher && cipher.decryptionFailure;
+ }
+
+ async restore(cipher: PopupCipherViewLike) {
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
- await this.cipherService.restoreWithServer(cipher.id, activeUserId);
+ await this.cipherService.restoreWithServer(cipher.id as string, activeUserId);
await this.router.navigate(["/trash"]);
this.toastService.showToast({
@@ -101,7 +130,7 @@ export class TrashListItemsContainerComponent {
}
}
- async delete(cipher: CipherView) {
+ async delete(cipher: PopupCipherViewLike) {
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
if (!repromptPassed) {
@@ -120,7 +149,7 @@ export class TrashListItemsContainerComponent {
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
- await this.cipherService.deleteWithServer(cipher.id, activeUserId);
+ await this.cipherService.deleteWithServer(cipher.id as string, activeUserId);
await this.router.navigate(["/trash"]);
this.toastService.showToast({
@@ -133,8 +162,9 @@ export class TrashListItemsContainerComponent {
}
}
- async onViewCipher(cipher: CipherView) {
- if (cipher.decryptionFailure) {
+ async onViewCipher(cipher: PopupCipherViewLike) {
+ // CipherListView doesn't have decryptionFailure, so we use optional chaining
+ if ("decryptionFailure" in cipher && cipher.decryptionFailure) {
DecryptionFailureDialogComponent.open(this.dialogService, {
cipherIds: [cipher.id as CipherId],
});
@@ -147,7 +177,7 @@ export class TrashListItemsContainerComponent {
}
await this.router.navigate(["/view-cipher"], {
- queryParams: { cipherId: cipher.id, type: cipher.type },
+ queryParams: { cipherId: cipher.id as string, type: cipher.type },
});
}
}
diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json
index 0fd6cac4230..6fb9dfbe46b 100644
--- a/apps/browser/tsconfig.json
+++ b/apps/browser/tsconfig.json
@@ -1,5 +1,8 @@
{
"extends": "../../tsconfig.base",
+ "angularCompilerOptions": {
+ "strictTemplates": true
+ },
"include": [
"src",
"../../libs/common/src/autofill/constants",
diff --git a/libs/vault/src/components/can-delete-cipher.directive.ts b/libs/vault/src/components/can-delete-cipher.directive.ts
index 8ab59f9d647..f88e38049da 100644
--- a/libs/vault/src/components/can-delete-cipher.directive.ts
+++ b/libs/vault/src/components/can-delete-cipher.directive.ts
@@ -1,8 +1,8 @@
import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
-import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
+import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
/**
* Only shows the element if the user can delete the cipher.
@@ -15,7 +15,7 @@ export class CanDeleteCipherDirective implements OnDestroy {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
- @Input("appCanDeleteCipher") set cipher(cipher: CipherView) {
+ @Input("appCanDeleteCipher") set cipher(cipher: CipherViewLike) {
this.viewContainer.clear();
this.cipherAuthorizationService
diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts
index b4b1941273f..52a4f59e7a2 100644
--- a/libs/vault/src/components/copy-cipher-field.directive.ts
+++ b/libs/vault/src/components/copy-cipher-field.directive.ts
@@ -36,7 +36,7 @@ export class CopyCipherFieldDirective implements OnChanges {
alias: "appCopyField",
required: true,
})
- action!: Exclude;
+ action!: CopyAction;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts
index ccd830cd34e..93a72ba14e0 100644
--- a/libs/vault/src/index.ts
+++ b/libs/vault/src/index.ts
@@ -3,7 +3,11 @@ export {
AtRiskPasswordCalloutData,
} from "./services/at-risk-password-callout.service";
export { PasswordRepromptService } from "./services/password-reprompt.service";
-export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
+export {
+ CopyCipherFieldService,
+ CopyAction,
+ CopyFieldAction,
+} from "./services/copy-cipher-field.service";
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
export { OrgIconDirective } from "./components/org-icon.directive";
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts
index 0a7d9e1f68d..539c81b7be3 100644
--- a/libs/vault/src/services/copy-cipher-field.service.ts
+++ b/libs/vault/src/services/copy-cipher-field.service.ts
@@ -35,6 +35,12 @@ export type CopyAction =
| "publicKey"
| "keyFingerprint";
+/**
+ * Copy actions that can be used with the appCopyField directive.
+ * Excludes "hiddenField" which requires special handling.
+ */
+export type CopyFieldAction = Exclude;
+
type CopyActionInfo = {
/**
* The i18n key for the type of field being copied. Will be used to display a toast message.