[PM-34108] Add Driver's License to browser (#20638)

* [PM-34108] Add Driver's License copy actions to browser vault list

* [PM-34108] Add missing Driver's License i18n keys to browser en messages

* prefer else syntax
This commit is contained in:
Nick Krantz 2026-05-14 09:34:16 -05:00 committed by GitHub
parent 7f25ac0aba
commit e3a22ccd34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 192 additions and 0 deletions

View File

@ -2196,6 +2196,48 @@
"typeDriversLicenseSubtitle": {
"message": "Drivers license or ID"
},
"driversLicenseDetails": {
"message": "License details"
},
"dateOfBirth": {
"message": "Date of birth"
},
"birthMonth": {
"message": "Birth month"
},
"birthDay": {
"message": "Birth day"
},
"birthYear": {
"message": "Birth year"
},
"issuingCountry": {
"message": "Issuing country"
},
"issuingState": {
"message": "Issuing state / province"
},
"issuingAuthority": {
"message": "Issuing authority"
},
"issueDate": {
"message": "Issue date"
},
"issueMonth": {
"message": "Issue month"
},
"issueDay": {
"message": "Issue day"
},
"issueYear": {
"message": "Issue year"
},
"expirationDay": {
"message": "Expiration day"
},
"licenseClass": {
"message": "License class"
},
"newItemHeaderLogin": {
"message": "New Login",
"description": "Header for new login item type"

View File

@ -213,3 +213,46 @@
</bit-menu>
</bit-item-action>
}
@if (CipherViewLikeUtils.getType(_cipher) === CipherType.DriversLicense) {
<bit-item-action>
@if (singleCopyableDriversLicense) {
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[label]="'copyFieldCipherName' | i18n: singleCopyableDriversLicense.key : _cipher.name"
[appCopyField]="singleCopyableDriversLicense.field"
[cipher]="_cipher"
showToast
></button>
} @else {
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[label]="
hasDriversLicenseValues
? ('copyInfoTitle' | i18n: _cipher.name)
: ('noValuesToCopy' | i18n)
"
[disabled]="!hasDriversLicenseValues"
[bitMenuTriggerFor]="driversLicenseOptions"
></button>
<bit-menu #driversLicenseOptions>
<button type="button" bitMenuItem appCopyField="firstName" [cipher]="_cipher">
{{ "copyFirstName" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="middleName" [cipher]="_cipher">
{{ "copyMiddleName" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="lastName" [cipher]="_cipher">
{{ "copyLastName" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="licenseNumber" [cipher]="_cipher">
{{ "copyLicenseNumber" | i18n }}
</button>
</bit-menu>
}
</bit-item-action>
}

View File

@ -293,6 +293,48 @@ describe("VaultItemCopyActionsComponent", () => {
});
});
describe("singleCopyableDriversLicense", () => {
beforeEach(() => {
jest
.spyOn(CipherViewLikeUtils, "hasCopyableValue")
.mockImplementation(
(cipher: CipherViewLike & { __copyable?: Record<string, boolean> }, field) => {
return Boolean(cipher.__copyable?.[field]);
},
);
});
it("returns the only copyable drivers license field", () => {
(component.cipher() as any).__copyable = {
firstName: false,
middleName: false,
lastName: false,
licenseNumber: true,
};
const result = component.singleCopyableDriversLicense;
expect(result).toEqual({
key: "translated-licenseNumber",
field: "licenseNumber",
});
expect(i18nService.t).toHaveBeenCalledWith("licenseNumber");
});
it("returns null when multiple drivers license fields are available", () => {
(component.cipher() as any).__copyable = {
firstName: true,
middleName: false,
lastName: true,
licenseNumber: false,
};
const result = component.singleCopyableDriversLicense;
expect(result).toBeNull();
});
});
describe("has*Values in non-list view", () => {
beforeEach(() => {
jest.spyOn(CipherViewLikeUtils, "isCipherListView").mockReturnValue(false);
@ -391,6 +433,26 @@ describe("VaultItemCopyActionsComponent", () => {
expect(component.hasSshKeyValues).toBe(false);
});
it("computes hasDriversLicenseValues from driversLicense fields", () => {
(component.cipher() as CipherView).driversLicense = {
firstName: "John",
middleName: null,
lastName: null,
licenseNumber: null,
} as any;
expect(component.hasDriversLicenseValues).toBe(true);
(component.cipher() as CipherView).driversLicense = {
firstName: null,
middleName: null,
lastName: null,
licenseNumber: null,
} as any;
expect(component.hasDriversLicenseValues).toBe(false);
});
});
describe("has*Values in list view", () => {
@ -463,5 +525,19 @@ describe("VaultItemCopyActionsComponent", () => {
expect(component.hasSshKeyValues).toBe(false);
});
it("uses copyableFields for drivers license values", () => {
(component.cipher() as CipherListView).copyableFields = [
"DriversLicenseLicenseNumber",
] as CopyableCipherFields[];
expect(component.hasDriversLicenseValues).toBe(true);
(component.cipher() as CipherListView).copyableFields = [
"LoginUsername",
] as CopyableCipherFields[];
expect(component.hasDriversLicenseValues).toBe(false);
});
});
});

View File

@ -76,6 +76,16 @@ export class VaultItemCopyActionsComponent {
return this.findSingleCopyableItem(this.cipher(), identityItems);
}
get singleCopyableDriversLicense() {
const driversLicenseItems: CipherItem[] = [
{ key: "firstName", field: "firstName" },
{ key: "middleName", field: "middleName" },
{ key: "lastName", field: "lastName" },
{ key: "licenseNumber", field: "licenseNumber" },
];
return this.findSingleCopyableItem(this.cipher(), driversLicenseItems);
}
/*
* Given a list of CipherItems, if there is only one item with a value,
* return it with the translated key. Otherwise return null.
@ -110,6 +120,10 @@ export class VaultItemCopyActionsComponent {
return this.getNumberOfSshKeyValues(this.cipher()) > 0;
}
get hasDriversLicenseValues() {
return this.getNumberOfDriversLicenseValues(this.cipher()) > 0;
}
/** Sets the number of populated login values for the cipher */
private getNumberOfLoginValues(cipher: CipherViewLike) {
return this.getLoginCopyableItems(cipher)
@ -168,4 +182,21 @@ export class VaultItemCopyActionsComponent {
Boolean,
).length;
}
/** Sets the number of populated drivers license values for the cipher */
private getNumberOfDriversLicenseValues(cipher: CipherViewLike) {
if (CipherViewLikeUtils.isCipherListView(cipher)) {
const copyableDriversLicenseFields: CopyableCipherFields[] = ["DriversLicenseLicenseNumber"];
return cipher.copyableFields.filter((field) => copyableDriversLicenseFields.includes(field))
.length;
}
return [
cipher.driversLicense?.firstName,
cipher.driversLicense?.middleName,
cipher.driversLicense?.lastName,
cipher.driversLicense?.licenseNumber,
].filter(Boolean).length;
}
}