diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 8b9ce12afd..432479aa80 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -116,6 +116,21 @@
"copySecurityCode": {
"message": "Copy security code"
},
+ "copyName": {
+ "message": "Copy name"
+ },
+ "copyCompany": {
+ "message": "Copy company"
+ },
+ "copySSN": {
+ "message": "Copy Social Security number"
+ },
+ "copyPassportNumber": {
+ "message": "Copy passport number"
+ },
+ "copyLicenseNumber": {
+ "message": "Copy license number"
+ },
"autoFill": {
"message": "Autofill"
},
diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html
index 5ef7f12341..1463cbd731 100644
--- a/libs/vault/src/cipher-view/cipher-view.component.html
+++ b/libs/vault/src/cipher-view/cipher-view.component.html
@@ -8,6 +8,10 @@
>
+
+
+
+
diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts
index 6e208d6f28..7e72424e1f 100644
--- a/libs/vault/src/cipher-view/cipher-view.component.ts
+++ b/libs/vault/src/cipher-view/cipher-view.component.ts
@@ -22,6 +22,7 @@ import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.co
import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component";
import { ItemDetailsV2Component } from "./item-details/item-details-v2.component";
import { ItemHistoryV2Component } from "./item-history/item-history-v2.component";
+import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component";
@Component({
selector: "app-cipher-view",
@@ -39,6 +40,7 @@ import { ItemHistoryV2Component } from "./item-history/item-history-v2.component
AttachmentsV2ViewComponent,
ItemHistoryV2Component,
CustomFieldV2Component,
+ ViewIdentitySectionsComponent,
],
})
export class CipherViewComponent implements OnInit {
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html
new file mode 100644
index 0000000000..742f3a5efe
--- /dev/null
+++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html
@@ -0,0 +1,161 @@
+
+
+ {{ "personalDetails" | i18n }}
+
+
+
+
+ {{ "name" | i18n }}
+
+
+
+
+ {{ "username" | i18n }}
+
+
+
+
+ {{ "company" | i18n }}
+
+
+
+
+
+
+
+
+ {{ "identification" | i18n }}
+
+
+
+
+ {{ "ssn" | i18n }}
+
+
+
+
+
+ {{ "passportNumber" | i18n }}
+
+
+
+
+
+ {{ "licenseNumber" | i18n }}
+
+
+
+
+
+
+
+
+ {{ "contactInfo" | i18n }}
+
+
+
+
+ {{ "email" | i18n }}
+
+
+
+
+ {{ "phone" | i18n }}
+
+
+
+
+ {{ "address" | i18n }}
+
+
+
+
+
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts
new file mode 100644
index 0000000000..4c84b2852a
--- /dev/null
+++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts
@@ -0,0 +1,200 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+import { mock } from "jest-mock-extended";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
+import { SectionHeaderComponent } from "@bitwarden/components";
+
+import { BitInputDirective } from "../../../../components/src/input/input.directive";
+
+import { ViewIdentitySectionsComponent } from "./view-identity-sections.component";
+
+describe("ViewIdentitySectionsComponent", () => {
+ let component: ViewIdentitySectionsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ViewIdentitySectionsComponent],
+ providers: [
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ { provide: PlatformUtilsService, useValue: mock() },
+ ],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ViewIdentitySectionsComponent);
+ component = fixture.componentInstance;
+ component.cipher = { identity: new IdentityView() } as CipherView;
+ fixture.detectChanges();
+ });
+
+ describe("personal details", () => {
+ it("dynamically shows the section", () => {
+ let personalDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
+
+ expect(personalDetailSection).toBeNull();
+
+ component.cipher = {
+ identity: {
+ fullName: "Mr Ron Burgundy",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ personalDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
+
+ expect(personalDetailSection).not.toBeNull();
+ expect(personalDetailSection.nativeElement.textContent).toBe("personalDetails");
+ });
+
+ it("populates personal detail fields", () => {
+ component.cipher = {
+ identity: {
+ fullName: "Mr Ron Burgundy",
+ company: "Channel 4 News",
+ username: "ron.burgundy",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const fields = fixture.debugElement.queryAll(By.directive(BitInputDirective));
+
+ expect(fields[0].nativeElement.value).toBe("Mr Ron Burgundy");
+ expect(fields[1].nativeElement.value).toBe("ron.burgundy");
+ expect(fields[2].nativeElement.value).toBe("Channel 4 News");
+ });
+ });
+
+ describe("identification details", () => {
+ it("dynamically shows the section", () => {
+ let identificationDetailSection = fixture.debugElement.query(
+ By.directive(SectionHeaderComponent),
+ );
+
+ expect(identificationDetailSection).toBeNull();
+
+ component.cipher = {
+ identity: {
+ ssn: "123-45-6789",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ identificationDetailSection = fixture.debugElement.query(
+ By.directive(SectionHeaderComponent),
+ );
+
+ expect(identificationDetailSection).not.toBeNull();
+ expect(identificationDetailSection.nativeElement.textContent).toBe("identification");
+ });
+
+ it("populates identification detail fields", () => {
+ component.cipher = {
+ identity: {
+ ssn: "123-45-6789",
+ passportNumber: "998-765-4321",
+ licenseNumber: "404-HTTP",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const fields = fixture.debugElement.queryAll(By.directive(BitInputDirective));
+
+ expect(fields[0].nativeElement.value).toBe("123-45-6789");
+ expect(fields[1].nativeElement.value).toBe("998-765-4321");
+ expect(fields[2].nativeElement.value).toBe("404-HTTP");
+ });
+ });
+
+ describe("contact details", () => {
+ it("dynamically shows the section", () => {
+ let contactDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
+
+ expect(contactDetailSection).toBeNull();
+
+ component.cipher = {
+ identity: {
+ email: "jack@gnn.com",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ contactDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
+
+ expect(contactDetailSection).not.toBeNull();
+ expect(contactDetailSection.nativeElement.textContent).toBe("contactInfo");
+ });
+
+ it("populates contact detail fields", () => {
+ component.cipher = {
+ identity: {
+ email: "jack@gnn.com",
+ phone: "608-867-5309",
+ address1: "2920 Zoo Dr",
+ address2: "Exhibit 200",
+ address3: "Tree 7",
+ fullAddressPart2: "San Diego, CA 92101",
+ country: "USA",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const fields = fixture.debugElement.queryAll(By.directive(BitInputDirective));
+
+ expect(fields[0].nativeElement.value).toBe("jack@gnn.com");
+ expect(fields[1].nativeElement.value).toBe("608-867-5309");
+ expect(fields[2].nativeElement.value).toBe(
+ "2920 Zoo Dr\nExhibit 200\nTree 7\nSan Diego, CA 92101\nUSA",
+ );
+ });
+
+ it('returns the number of "rows" that should be assigned to the address textarea', () => {
+ component.cipher = {
+ identity: {
+ address1: "2920 Zoo Dr",
+ address2: "Exhibit 200",
+ address3: "Tree 7",
+ fullAddressPart2: "San Diego, CA 92101",
+ country: "USA",
+ },
+ } as CipherView;
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ let textarea = fixture.debugElement.query(By.css("textarea"));
+
+ expect(textarea.nativeElement.rows).toBe(5);
+
+ component.cipher = {
+ identity: {
+ address1: "2920 Zoo Dr",
+ country: "USA",
+ },
+ } as CipherView;
+
+ fixture.detectChanges();
+
+ textarea = fixture.debugElement.query(By.css("textarea"));
+
+ expect(textarea.nativeElement.rows).toBe(2);
+ });
+ });
+});
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts
new file mode 100644
index 0000000000..0fd2c29295
--- /dev/null
+++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts
@@ -0,0 +1,72 @@
+import { NgIf } from "@angular/common";
+import { Component, Input, OnInit } from "@angular/core";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import {
+ CardComponent,
+ FormFieldModule,
+ IconButtonModule,
+ SectionComponent,
+ SectionHeaderComponent,
+ TypographyModule,
+} from "@bitwarden/components";
+
+@Component({
+ standalone: true,
+ selector: "app-view-identity-sections",
+ templateUrl: "./view-identity-sections.component.html",
+ imports: [
+ NgIf,
+ JslibModule,
+ CardComponent,
+ SectionComponent,
+ SectionHeaderComponent,
+ TypographyModule,
+ FormFieldModule,
+ IconButtonModule,
+ ],
+})
+export class ViewIdentitySectionsComponent implements OnInit {
+ @Input() cipher: CipherView;
+
+ showPersonalDetails: boolean;
+ showIdentificationDetails: boolean;
+ showContactDetails: boolean;
+
+ ngOnInit(): void {
+ this.showPersonalDetails = this.hasPersonalDetails();
+ this.showIdentificationDetails = this.hasIdentificationDetails();
+ this.showContactDetails = this.hasContactDetails();
+ }
+
+ /** Returns all populated address fields */
+ get addressFields(): string {
+ const { address1, address2, address3, fullAddressPart2, country } = this.cipher.identity;
+ return [address1, address2, address3, fullAddressPart2, country].filter(Boolean).join("\n");
+ }
+
+ /** Returns the number of "rows" that should be assigned to the address textarea */
+ get addressRows(): number {
+ return this.addressFields.split("\n").length;
+ }
+
+ /** Returns true when any of the "personal detail" attributes are populated */
+ private hasPersonalDetails(): boolean {
+ const { username, company, fullName } = this.cipher.identity;
+ return Boolean(fullName || username || company);
+ }
+
+ /** Returns true when any of the "identification detail" attributes are populated */
+ private hasIdentificationDetails(): boolean {
+ const { ssn, passportNumber, licenseNumber } = this.cipher.identity;
+ return Boolean(ssn || passportNumber || licenseNumber);
+ }
+
+ /** Returns true when any of the "contact detail" attributes are populated */
+ private hasContactDetails(): boolean {
+ const { email, phone } = this.cipher.identity;
+
+ return Boolean(email || phone || this.addressFields);
+ }
+}