From 76351ce75048751130c9c118e36aa78b6a405f10 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Fri, 2 Aug 2024 14:14:23 -0500 Subject: [PATCH 01/31] [PM-10420] Autofill focus jumps around after autofilling identity (#10361) * [PM-10420] Autofill focus jumps around after autofilling identity ciphers * [PM-10420] Autofill focus jumps around after autofilling identity ciphers * [PM-10420] Autofill focus jumps around after autofilling identity ciphers * [PM-10420] Incorporating the feature flag within jest to test the validity of both implementations * [PM-10420] Refactoring how we compile the combined list of keywords * [PM-10420] Adding JSDocs to the implemented methods --- .../autofill/services/autofill-constants.ts | 5 + .../services/autofill.service.spec.ts | 783 +++++++++--------- .../src/autofill/services/autofill.service.ts | 600 +++++++++++++- ...inline-menu-field-qualification.service.ts | 7 +- libs/common/src/enums/feature-flag.enum.ts | 2 + 5 files changed, 977 insertions(+), 420 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill-constants.ts b/apps/browser/src/autofill/services/autofill-constants.ts index 3a297e8d251..22b248be77b 100644 --- a/apps/browser/src/autofill/services/autofill-constants.ts +++ b/apps/browser/src/autofill/services/autofill-constants.ts @@ -76,6 +76,11 @@ export class AutoFillConstants { "textarea", ...AutoFillConstants.ExcludedAutofillTypes, ]; + + static readonly ExcludedIdentityAutocompleteTypes: Set = new Set([ + "current-password", + "new-password", + ]); } export class CreditCardAutoFillConstants { diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 1ceebe53fca..aca82227284 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -60,7 +60,7 @@ import { GenerateFillScriptOptions, PageDetail, } from "./abstractions/autofill.service"; -import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants"; +import { AutoFillConstants } from "./autofill-constants"; import AutofillService from "./autofill.service"; const mockEquivalentDomains = [ @@ -3056,12 +3056,12 @@ describe("AutofillService", () => { options.cipher.identity = mock(); }); - it("returns null if an identify is not found within the cipher", () => { + it("returns null if an identify is not found within the cipher", async () => { options.cipher.identity = null; jest.spyOn(autofillService as any, "makeScriptAction"); jest.spyOn(autofillService as any, "makeScriptActionWithValue"); - const value = autofillService["generateIdentityFillScript"]( + const value = await autofillService["generateIdentityFillScript"]( fillScript, pageDetails, filledFields, @@ -3087,432 +3087,389 @@ describe("AutofillService", () => { jest.spyOn(autofillService as any, "makeScriptActionWithValue"); }); - it("will not attempt to match custom fields", () => { - const customField = createAutofillFieldMock({ tagName: "span" }); - pageDetails.fields.push(customField); + let isRefactorFeatureFlagSet = false; + for (let index = 0; index < 2; index++) { + describe(`when the isRefactorFeatureFlagSet is ${isRefactorFeatureFlagSet}`, () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(isRefactorFeatureFlagSet); + }); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + afterAll(() => { + isRefactorFeatureFlagSet = true; + }); - expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField); - expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); - expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); - expect(value.script).toStrictEqual([]); - }); + it("will not attempt to match custom fields", async () => { + const customField = createAutofillFieldMock({ tagName: "span" }); + pageDetails.fields.push(customField); - it("will not attempt to match a field that is of an excluded type", () => { - const excludedField = createAutofillFieldMock({ type: "hidden" }); - pageDetails.fields.push(excludedField); + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField); + expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); + expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); + expect(value.script).toStrictEqual([]); + }); - expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField); - expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith( - excludedField, - AutoFillConstants.ExcludedAutofillTypes, - ); - expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); - expect(value.script).toStrictEqual([]); - }); + it("will not attempt to match a field that is of an excluded type", async () => { + const excludedField = createAutofillFieldMock({ type: "hidden" }); + pageDetails.fields.push(excludedField); - it("will not attempt to match a field that is not viewable", () => { - const viewableField = createAutofillFieldMock({ viewable: false }); - pageDetails.fields.push(viewableField); + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField); + expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith( + excludedField, + AutoFillConstants.ExcludedAutofillTypes, + ); + expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); + expect(value.script).toStrictEqual([]); + }); - expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField); - expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); - expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); - expect(value.script).toStrictEqual([]); - }); + it("will not attempt to match a field that is not viewable", async () => { + const viewableField = createAutofillFieldMock({ viewable: false }); + pageDetails.fields.push(viewableField); - it("will match a full name field to the vault item identity value", () => { - const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" }); - pageDetails.fields = [fullNameField]; - options.cipher.identity.firstName = firstName; - options.cipher.identity.middleName = middleName; - options.cipher.identity.lastName = lastName; + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField); + expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); + expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); + expect(value.script).toStrictEqual([]); + }); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - fullNameField.htmlName, - IdentityAutoFillConstants.FullNameFieldNames, - IdentityAutoFillConstants.FullNameFieldNameValues, - ); - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - `${firstName} ${middleName} ${lastName}`, - fullNameField, - filledFields, - ); - expect(value.script[2]).toStrictEqual([ - "fill_by_opid", - fullNameField.opid, - `${firstName} ${middleName} ${lastName}`, - ]); - }); + it("will match a full name field to the vault item identity value", async () => { + const fullNameField = createAutofillFieldMock({ + opid: "fullName", + htmlName: "full-name", + }); + pageDetails.fields = [fullNameField]; + options.cipher.identity.firstName = firstName; + options.cipher.identity.middleName = middleName; + options.cipher.identity.lastName = lastName; - it("will match a full name field to the a vault item that only has a last name", () => { - const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" }); - pageDetails.fields = [fullNameField]; - options.cipher.identity.firstName = ""; - options.cipher.identity.middleName = ""; - options.cipher.identity.lastName = lastName; + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + `${firstName} ${middleName} ${lastName}`, + fullNameField, + filledFields, + ); + expect(value.script[2]).toStrictEqual([ + "fill_by_opid", + fullNameField.opid, + `${firstName} ${middleName} ${lastName}`, + ]); + }); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - fullNameField.htmlName, - IdentityAutoFillConstants.FullNameFieldNames, - IdentityAutoFillConstants.FullNameFieldNameValues, - ); - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - lastName, - fullNameField, - filledFields, - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]); - }); + it("will match a full name field to the a vault item that only has a last name", async () => { + const fullNameField = createAutofillFieldMock({ + opid: "fullName", + htmlName: "full-name", + }); + pageDetails.fields = [fullNameField]; + options.cipher.identity.firstName = ""; + options.cipher.identity.middleName = ""; + options.cipher.identity.lastName = lastName; - it("will match first name, middle name, and last name fields to the vault item identity value", () => { - const firstNameField = createAutofillFieldMock({ - opid: "firstName", - htmlName: "first-name", + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + lastName, + fullNameField, + filledFields, + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]); + }); + + it("will match first name, middle name, and last name fields to the vault item identity value", async () => { + const firstNameField = createAutofillFieldMock({ + opid: "firstName", + htmlName: "first-name", + }); + const middleNameField = createAutofillFieldMock({ + opid: "middleName", + htmlName: "middle-name", + }); + const lastNameField = createAutofillFieldMock({ + opid: "lastName", + htmlName: "last-name", + }); + pageDetails.fields = [firstNameField, middleNameField, lastNameField]; + options.cipher.identity.firstName = firstName; + options.cipher.identity.middleName = middleName; + options.cipher.identity.lastName = lastName; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.firstName, + firstNameField, + filledFields, + ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.middleName, + middleNameField, + filledFields, + ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.lastName, + lastNameField, + filledFields, + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]); + expect(value.script[5]).toStrictEqual([ + "fill_by_opid", + middleNameField.opid, + middleName, + ]); + expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]); + }); + + it("will match title and email fields to the vault item identity value", async () => { + const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" }); + const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" }); + pageDetails.fields = [titleField, emailField]; + const title = "Mr."; + const email = "email@example.com"; + options.cipher.identity.title = title; + options.cipher.identity.email = email; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.title, + titleField, + filledFields, + ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.email, + emailField, + filledFields, + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]); + expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]); + }); + + it("will match a full address field to the vault item identity values", async () => { + const fullAddressField = createAutofillFieldMock({ + opid: "fullAddress", + htmlName: "address", + }); + pageDetails.fields = [fullAddressField]; + const address1 = "123 Main St."; + const address2 = "Apt. 1"; + const address3 = "P.O. Box 123"; + options.cipher.identity.address1 = address1; + options.cipher.identity.address2 = address2; + options.cipher.identity.address3 = address3; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + `${address1}, ${address2}, ${address3}`, + fullAddressField, + filledFields, + ); + expect(value.script[2]).toStrictEqual([ + "fill_by_opid", + fullAddressField.opid, + `${address1}, ${address2}, ${address3}`, + ]); + }); + + it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", async () => { + const address1Field = createAutofillFieldMock({ + opid: "address1", + htmlName: "address-1", + }); + const address2Field = createAutofillFieldMock({ + opid: "address2", + htmlName: "address-2", + }); + const address3Field = createAutofillFieldMock({ + opid: "address3", + htmlName: "address-3", + }); + const postalCodeField = createAutofillFieldMock({ + opid: "postalCode", + htmlName: "postal-code", + }); + const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" }); + const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); + const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); + const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" }); + const usernameField = createAutofillFieldMock({ + opid: "username", + htmlName: "username", + }); + const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" }); + pageDetails.fields = [ + address1Field, + address2Field, + address3Field, + postalCodeField, + cityField, + stateField, + countryField, + phoneField, + usernameField, + companyField, + ]; + const address1 = "123 Main St."; + const address2 = "Apt. 1"; + const address3 = "P.O. Box 123"; + const postalCode = "12345"; + const city = "City"; + const state = "TX"; + const country = "US"; + const phone = "123-456-7890"; + const username = "username"; + const company = "Company"; + options.cipher.identity.address1 = address1; + options.cipher.identity.address2 = address2; + options.cipher.identity.address3 = address3; + options.cipher.identity.postalCode = postalCode; + options.cipher.identity.city = city; + options.cipher.identity.state = state; + options.cipher.identity.country = country; + options.cipher.identity.phone = phone; + options.cipher.identity.username = username; + options.cipher.identity.company = company; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(value.script).toContainEqual(["fill_by_opid", address1Field.opid, address1]); + expect(value.script).toContainEqual(["fill_by_opid", address2Field.opid, address2]); + expect(value.script).toContainEqual(["fill_by_opid", address3Field.opid, address3]); + expect(value.script).toContainEqual(["fill_by_opid", postalCodeField.opid, postalCode]); + expect(value.script).toContainEqual(["fill_by_opid", cityField.opid, city]); + expect(value.script).toContainEqual(["fill_by_opid", stateField.opid, state]); + expect(value.script).toContainEqual(["fill_by_opid", countryField.opid, country]); + expect(value.script).toContainEqual(["fill_by_opid", phoneField.opid, phone]); + expect(value.script).toContainEqual(["fill_by_opid", usernameField.opid, username]); + expect(value.script).toContainEqual(["fill_by_opid", companyField.opid, company]); + }); + + it("will find the two character IsoState value for an identity cipher that contains the full name of a state", async () => { + const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); + pageDetails.fields = [stateField]; + const state = "California"; + options.cipher.identity.state = state; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + "CA", + expect.anything(), + expect.anything(), + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]); + }); + + it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", async () => { + const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); + pageDetails.fields = [stateField]; + const state = "Ontario"; + options.cipher.identity.state = state; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + "ON", + expect.anything(), + expect.anything(), + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]); + }); + + it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", async () => { + const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); + pageDetails.fields = [countryField]; + const country = "Somalia"; + options.cipher.identity.country = country; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + "SO", + expect.anything(), + expect.anything(), + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]); + }); }); - const middleNameField = createAutofillFieldMock({ - opid: "middleName", - htmlName: "middle-name", - }); - const lastNameField = createAutofillFieldMock({ opid: "lastName", htmlName: "last-name" }); - pageDetails.fields = [firstNameField, middleNameField, lastNameField]; - options.cipher.identity.firstName = firstName; - options.cipher.identity.middleName = middleName; - options.cipher.identity.lastName = lastName; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - firstNameField.htmlName, - IdentityAutoFillConstants.FirstnameFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - middleNameField.htmlName, - IdentityAutoFillConstants.MiddlenameFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - lastNameField.htmlName, - IdentityAutoFillConstants.LastnameFieldNames, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - firstNameField.opid, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - middleNameField.opid, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - lastNameField.opid, - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]); - expect(value.script[5]).toStrictEqual(["fill_by_opid", middleNameField.opid, middleName]); - expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]); - }); - - it("will match title and email fields to the vault item identity value", () => { - const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" }); - const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" }); - pageDetails.fields = [titleField, emailField]; - const title = "Mr."; - const email = "email@example.com"; - options.cipher.identity.title = title; - options.cipher.identity.email = email; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - titleField.htmlName, - IdentityAutoFillConstants.TitleFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - emailField.htmlName, - IdentityAutoFillConstants.EmailFieldNames, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - titleField.opid, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - emailField.opid, - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]); - expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]); - }); - - it("will match a full address field to the vault item identity values", () => { - const fullAddressField = createAutofillFieldMock({ - opid: "fullAddress", - htmlName: "address", - }); - pageDetails.fields = [fullAddressField]; - const address1 = "123 Main St."; - const address2 = "Apt. 1"; - const address3 = "P.O. Box 123"; - options.cipher.identity.address1 = address1; - options.cipher.identity.address2 = address2; - options.cipher.identity.address3 = address3; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - fullAddressField.htmlName, - IdentityAutoFillConstants.AddressFieldNames, - IdentityAutoFillConstants.AddressFieldNameValues, - ); - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - `${address1}, ${address2}, ${address3}`, - fullAddressField, - filledFields, - ); - expect(value.script[2]).toStrictEqual([ - "fill_by_opid", - fullAddressField.opid, - `${address1}, ${address2}, ${address3}`, - ]); - }); - - it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", () => { - const address1Field = createAutofillFieldMock({ opid: "address1", htmlName: "address-1" }); - const address2Field = createAutofillFieldMock({ opid: "address2", htmlName: "address-2" }); - const address3Field = createAutofillFieldMock({ opid: "address3", htmlName: "address-3" }); - const postalCodeField = createAutofillFieldMock({ - opid: "postalCode", - htmlName: "postal-code", - }); - const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" }); - const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); - const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); - const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" }); - const usernameField = createAutofillFieldMock({ opid: "username", htmlName: "username" }); - const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" }); - pageDetails.fields = [ - address1Field, - address2Field, - address3Field, - postalCodeField, - cityField, - stateField, - countryField, - phoneField, - usernameField, - companyField, - ]; - const address1 = "123 Main St."; - const address2 = "Apt. 1"; - const address3 = "P.O. Box 123"; - const postalCode = "12345"; - const city = "City"; - const state = "State"; - const country = "Country"; - const phone = "123-456-7890"; - const username = "username"; - const company = "Company"; - options.cipher.identity.address1 = address1; - options.cipher.identity.address2 = address2; - options.cipher.identity.address3 = address3; - options.cipher.identity.postalCode = postalCode; - options.cipher.identity.city = city; - options.cipher.identity.state = state; - options.cipher.identity.country = country; - options.cipher.identity.phone = phone; - options.cipher.identity.username = username; - options.cipher.identity.company = company; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - address1Field.htmlName, - IdentityAutoFillConstants.Address1FieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - address2Field.htmlName, - IdentityAutoFillConstants.Address2FieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - address3Field.htmlName, - IdentityAutoFillConstants.Address3FieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - postalCodeField.htmlName, - IdentityAutoFillConstants.PostalCodeFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - cityField.htmlName, - IdentityAutoFillConstants.CityFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - stateField.htmlName, - IdentityAutoFillConstants.StateFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - countryField.htmlName, - IdentityAutoFillConstants.CountryFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - phoneField.htmlName, - IdentityAutoFillConstants.PhoneFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - usernameField.htmlName, - IdentityAutoFillConstants.UserNameFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - companyField.htmlName, - IdentityAutoFillConstants.CompanyFieldNames, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalled(); - expect(value.script[2]).toStrictEqual(["fill_by_opid", address1Field.opid, address1]); - expect(value.script[5]).toStrictEqual(["fill_by_opid", address2Field.opid, address2]); - expect(value.script[8]).toStrictEqual(["fill_by_opid", address3Field.opid, address3]); - expect(value.script[11]).toStrictEqual(["fill_by_opid", cityField.opid, city]); - expect(value.script[14]).toStrictEqual(["fill_by_opid", postalCodeField.opid, postalCode]); - expect(value.script[17]).toStrictEqual(["fill_by_opid", companyField.opid, company]); - expect(value.script[20]).toStrictEqual(["fill_by_opid", phoneField.opid, phone]); - expect(value.script[23]).toStrictEqual(["fill_by_opid", usernameField.opid, username]); - expect(value.script[26]).toStrictEqual(["fill_by_opid", stateField.opid, state]); - expect(value.script[29]).toStrictEqual(["fill_by_opid", countryField.opid, country]); - }); - - it("will find the two character IsoState value for an identity cipher that contains the full name of a state", () => { - const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); - pageDetails.fields = [stateField]; - const state = "California"; - options.cipher.identity.state = state; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - "CA", - expect.anything(), - expect.anything(), - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]); - }); - - it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", () => { - const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); - pageDetails.fields = [stateField]; - const state = "Ontario"; - options.cipher.identity.state = state; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - "ON", - expect.anything(), - expect.anything(), - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]); - }); - - it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", () => { - const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); - pageDetails.fields = [countryField]; - const country = "Somalia"; - options.cipher.identity.country = country; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - "SO", - expect.anything(), - expect.anything(), - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]); - }); + } }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index f2ef9790f62..d9ae4e99237 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -26,6 +26,7 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; @@ -478,6 +479,12 @@ export default class AutofillService implements AutofillServiceInterface { return totpCode; } + /** + * Checks if the cipher requires password reprompt and opens the password reprompt popout if necessary. + * + * @param cipher - The cipher to autofill + * @param tab - The tab to autofill + */ async isPasswordRepromptRequired(cipher: CipherView, tab: chrome.tabs.Tab): Promise { const userHasMasterPasswordAndKeyHash = await this.userVerificationService.hasMasterPasswordAndMasterKeyHash(); @@ -654,7 +661,7 @@ export default class AutofillService implements AutofillServiceInterface { fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options); break; case CipherType.Identity: - fillScript = this.generateIdentityFillScript( + fillScript = await this.generateIdentityFillScript( fillScript, pageDetails, filledFields, @@ -1243,12 +1250,16 @@ export default class AutofillService implements AutofillServiceInterface { * @returns {AutofillScript} * @private */ - private generateIdentityFillScript( + private async generateIdentityFillScript( fillScript: AutofillScript, pageDetails: AutofillPageDetails, filledFields: { [id: string]: AutofillField }, options: GenerateFillScriptOptions, - ): AutofillScript { + ): Promise { + if (await this.configService.getFeatureFlag(FeatureFlag.GenerateIdentityFillScriptRefactor)) { + return this._generateIdentityFillScript(fillScript, pageDetails, filledFields, options); + } + if (!options.cipher.identity) { return null; } @@ -1476,6 +1487,589 @@ export default class AutofillService implements AutofillServiceInterface { return fillScript; } + /** + * Generates the autofill script for the specified page details and identity cipher item. + * + * @param fillScript - Object to store autofill script, passed between method references + * @param pageDetails - The details of the page to autofill + * @param filledFields - The fields that have already been filled, passed between method references + * @param options - Contains data used to fill cipher items + */ + private _generateIdentityFillScript( + fillScript: AutofillScript, + pageDetails: AutofillPageDetails, + filledFields: { [id: string]: AutofillField }, + options: GenerateFillScriptOptions, + ): AutofillScript { + const identity = options.cipher.identity; + if (!identity) { + return null; + } + + for (let fieldsIndex = 0; fieldsIndex < pageDetails.fields.length; fieldsIndex++) { + const field = pageDetails.fields[fieldsIndex]; + if (this.excludeFieldFromIdentityFill(field)) { + continue; + } + + const keywordsList = this.getIdentityAutofillFieldKeywords(field); + const keywordsCombined = keywordsList.join(","); + if (this.shouldMakeIdentityTitleFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.title, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityNameFillScript(filledFields, keywordsList)) { + this.makeIdentityNameFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityFirstNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.firstName, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityMiddleNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.middleName, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityLastNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.lastName, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityEmailFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.email, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityAddressFillScript(filledFields, keywordsList)) { + this.makeIdentityAddressFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityAddress1FillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.address1, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityAddress2FillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.address2, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityAddress3FillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.address3, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityPostalCodeFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.postalCode, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityCityFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.city, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityStateFillScript(filledFields, keywordsCombined)) { + this.makeIdentityStateFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityCountryFillScript(filledFields, keywordsCombined)) { + this.makeIdentityCountryFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityPhoneFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.phone, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityUserNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.username, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityCompanyFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.company, field, filledFields); + } + } + + return fillScript; + } + + /** + * Identifies if the current field should be excluded from triggering autofill of the identity cipher. + * + * @param field - The field to check + */ + private excludeFieldFromIdentityFill(field: AutofillField): boolean { + return ( + AutofillService.isExcludedFieldType(field, AutoFillConstants.ExcludedAutofillTypes) || + AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) || + !field.viewable + ); + } + + /** + * Gathers all unique keyword identifiers from a field that can be used to determine what + * identity value should be filled. + * + * @param field - The field to gather keywords from + */ + private getIdentityAutofillFieldKeywords(field: AutofillField): string[] { + const keywords: Set = new Set(); + for (let index = 0; index < IdentityAutoFillConstants.IdentityAttributes.length; index++) { + const attribute = IdentityAutoFillConstants.IdentityAttributes[index]; + if (field[attribute]) { + keywords.add( + field[attribute] + .trim() + .toLowerCase() + .replace(/[^a-zA-Z0-9]+/g, ""), + ); + } + } + + return Array.from(keywords); + } + + /** + * Identifies if a fill script action for the identity title + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityTitleFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.title && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.TitleFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityNameFillScript( + filledFields: Record, + keywords: string[], + ): boolean { + return ( + !filledFields.name && + keywords.some((keyword) => + AutofillService.isFieldMatch( + keyword, + IdentityAutoFillConstants.FullNameFieldNames, + IdentityAutoFillConstants.FullNameFieldNameValues, + ), + ) + ); + } + + /** + * Identifies if a fill script action for the identity first name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityFirstNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.firstName && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.FirstnameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity middle name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityMiddleNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.middleName && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.MiddlenameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity last name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityLastNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.lastName && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.LastnameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity email + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityEmailFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.email && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.EmailFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity address + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddressFillScript( + filledFields: Record, + keywords: string[], + ): boolean { + return ( + !filledFields.address && + keywords.some((keyword) => + AutofillService.isFieldMatch( + keyword, + IdentityAutoFillConstants.AddressFieldNames, + IdentityAutoFillConstants.AddressFieldNameValues, + ), + ) + ); + } + + /** + * Identifies if a fill script action for the identity address1 + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddress1FillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.address1 && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address1FieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity address2 + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddress2FillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.address2 && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address2FieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity address3 + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddress3FillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.address3 && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address3FieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity postal code + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityPostalCodeFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.postalCode && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PostalCodeFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity city + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityCityFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.city && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CityFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity state + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityStateFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.state && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.StateFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity country + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityCountryFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.country && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CountryFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity phone + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityPhoneFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.phone && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PhoneFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity username + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityUserNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.username && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.UserNameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity company + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityCompanyFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.company && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CompanyFieldNames) + ); + } + + /** + * Creates an identity name fill script action for the provided field. This is used + * when filling a `full name` field, using the first, middle, and last name from the + * identity cipher item. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityNameFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + let name = ""; + if (identity.firstName) { + name += identity.firstName; + } + + if (identity.middleName) { + name += !name ? identity.middleName : ` ${identity.middleName}`; + } + + if (identity.lastName) { + name += !name ? identity.lastName : ` ${identity.lastName}`; + } + + this.makeScriptActionWithValue(fillScript, name, field, filledFields); + } + + /** + * Creates an identity address fill script action for the provided field. This is used + * when filling a generic `address` field, using the address1, address2, and address3 + * from the identity cipher item. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityAddressFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + if (!identity.address1) { + return; + } + + let address = identity.address1; + + if (identity.address2) { + address += `, ${identity.address2}`; + } + + if (identity.address3) { + address += `, ${identity.address3}`; + } + + this.makeScriptActionWithValue(fillScript, address, field, filledFields); + } + + /** + * Creates an identity state fill script action for the provided field. This is used + * when filling a `state` field, using the state value from the identity cipher item. + * If the state value is a full name, it will be converted to an ISO code. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityStateFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + if (!identity.state) { + return; + } + + if (identity.state.length <= 2) { + this.makeScriptActionWithValue(fillScript, identity.state, field, filledFields); + return; + } + + const stateLower = identity.state.toLowerCase(); + const isoState = + IdentityAutoFillConstants.IsoStates[stateLower] || + IdentityAutoFillConstants.IsoProvinces[stateLower]; + if (isoState) { + this.makeScriptActionWithValue(fillScript, isoState, field, filledFields); + } + } + + /** + * Creates an identity country fill script action for the provided field. This is used + * when filling a `country` field, using the country value from the identity cipher item. + * If the country value is a full name, it will be converted to an ISO code. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityCountryFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + if (!identity.country) { + return; + } + + if (identity.country.length <= 2) { + this.makeScriptActionWithValue(fillScript, identity.country, field, filledFields); + return; + } + + const countryLower = identity.country.toLowerCase(); + const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower]; + if (isoCountry) { + this.makeScriptActionWithValue(fillScript, isoCountry, field, filledFields); + } + } + /** * Accepts an HTMLInputElement type value and a list of * excluded types and returns true if the type is excluded. diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 3c48d8db83f..1af8cd5bd28 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -1057,7 +1057,7 @@ export class InlineMenuFieldQualificationService returnStringValue: boolean, ) { if (!this.autofillFieldKeywordsMap.has(autofillFieldData)) { - const keywords = [ + const keywordsSet = new Set([ autofillFieldData.htmlID, autofillFieldData.htmlName, autofillFieldData.htmlClass, @@ -1071,9 +1071,8 @@ export class InlineMenuFieldQualificationService autofillFieldData["label-right"], autofillFieldData["label-tag"], autofillFieldData["label-top"], - ]; - const keywordsSet = new Set(keywords); - const stringValue = keywords.join(",").toLowerCase(); + ]); + const stringValue = Array.from(keywordsSet).join(",").toLowerCase(); this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue }); } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f077e5a554f..4b19251d979 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -29,6 +29,7 @@ export enum FeatureFlag { AuthenticatorTwoFactorToken = "authenticator-2fa-token", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub", + GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -68,6 +69,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AuthenticatorTwoFactorToken]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE, + [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 032c013dd7d5989818f54f690bf71680c4c802f4 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Fri, 2 Aug 2024 14:30:23 -0500 Subject: [PATCH 02/31] [PM-10550] Card inline menu not displayed on popup payment form (#10380) * [PM-10550] Card inline menu not displayed on popup payment form * [PM-10550] Removing requirement for autocomplete to be "off" for identity fields when matching inline menu presentation --- ...inline-menu-field-qualification.service.ts | 197 ++++++++---------- 1 file changed, 91 insertions(+), 106 deletions(-) diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 1af8cd5bd28..e9d5e70c1cd 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -209,10 +209,7 @@ export class InlineMenuFieldQualificationService return false; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, this.creditCardFieldKeywords) - ); + return this.keywordsFoundInFieldData(field, this.creditCardFieldKeywords); } // If the field has a parent form, check the fields from that form exclusively @@ -232,10 +229,7 @@ export class InlineMenuFieldQualificationService return false; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords]) - ); + return this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords]); } /** Validates the provided field as a field for an account creation form. @@ -264,10 +258,7 @@ export class InlineMenuFieldQualificationService // If no password fields are found on the page, check for keywords that indicate the field is // part of an account creation form. - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords) - ); + return this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords); } // If the field has a parent form, check the fields from that form exclusively @@ -277,10 +268,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords) - ); + return this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords); } /** @@ -298,10 +286,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, this.identityFieldKeywords) - ); + return this.keywordsFoundInFieldData(field, this.identityFieldKeywords); } /** @@ -480,9 +465,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardHolderFieldNames, false) + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.CardHolderFieldNames, + false, ); }; @@ -496,9 +482,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardNumberFieldNames, false) + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.CardNumberFieldNames, + false, ); }; @@ -514,9 +501,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardExpiryFieldNames, false) + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.CardExpiryFieldNames, + false, ); }; @@ -532,9 +520,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryMonthFieldNames, false) + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.ExpiryMonthFieldNames, + false, ); }; @@ -550,9 +539,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryYearFieldNames, false) + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.ExpiryYearFieldNames, + false, ); }; @@ -566,10 +556,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false); }; /** @@ -584,10 +571,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.TitleFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.TitleFieldNames, false); }; /** @@ -600,9 +584,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.FirstnameFieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.FirstnameFieldNames, + false, ); }; @@ -616,9 +601,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.MiddlenameFieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.MiddlenameFieldNames, + false, ); }; @@ -632,9 +618,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.LastnameFieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.LastnameFieldNames, + false, ); }; @@ -648,9 +635,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.FullNameFieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.FullNameFieldNames, + false, ); }; @@ -664,16 +652,13 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData( - field, - [ - ...IdentityAutoFillConstants.AddressFieldNames, - ...IdentityAutoFillConstants.Address1FieldNames, - ], - false, - ) + return this.keywordsFoundInFieldData( + field, + [ + ...IdentityAutoFillConstants.AddressFieldNames, + ...IdentityAutoFillConstants.Address1FieldNames, + ], + false, ); }; @@ -687,9 +672,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address2FieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.Address2FieldNames, + false, ); }; @@ -703,9 +689,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address3FieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.Address3FieldNames, + false, ); }; @@ -719,10 +706,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CityFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CityFieldNames, false); }; /** @@ -735,10 +719,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.StateFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.StateFieldNames, false); }; /** @@ -751,9 +732,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PostalCodeFieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.PostalCodeFieldNames, + false, ); }; @@ -767,10 +749,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CountryFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CountryFieldNames, false); }; /** @@ -783,10 +762,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CompanyFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CompanyFieldNames, false); }; /** @@ -799,10 +775,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PhoneFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PhoneFieldNames, false); }; /** @@ -818,10 +791,7 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.EmailFieldNames, false) - ); + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.EmailFieldNames, false); }; /** @@ -834,9 +804,10 @@ export class InlineMenuFieldQualificationService return true; } - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.UserNameFieldNames, false) + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.UserNameFieldNames, + false, ); }; @@ -1039,11 +1010,13 @@ export class InlineMenuFieldQualificationService fuzzyMatchKeywords = true, ) { const searchedValues = this.getAutofillFieldDataKeywords(autofillFieldData, fuzzyMatchKeywords); + const parsedKeywords = keywords.map((keyword) => keyword.replace(/-/g, "")); + if (typeof searchedValues === "string") { - return keywords.some((keyword) => searchedValues.indexOf(keyword) > -1); + return parsedKeywords.some((keyword) => searchedValues.indexOf(keyword) > -1); } - return keywords.some((keyword) => searchedValues.has(keyword)); + return parsedKeywords.some((keyword) => searchedValues.has(keyword)); } /** @@ -1057,7 +1030,7 @@ export class InlineMenuFieldQualificationService returnStringValue: boolean, ) { if (!this.autofillFieldKeywordsMap.has(autofillFieldData)) { - const keywordsSet = new Set([ + const keywords = [ autofillFieldData.htmlID, autofillFieldData.htmlName, autofillFieldData.htmlClass, @@ -1071,8 +1044,20 @@ export class InlineMenuFieldQualificationService autofillFieldData["label-right"], autofillFieldData["label-tag"], autofillFieldData["label-top"], - ]); - const stringValue = Array.from(keywordsSet).join(",").toLowerCase(); + ]; + const keywordsSet = new Set(); + for (let i = 0; i < keywords.length; i++) { + if (typeof keywords[i] === "string") { + keywords[i] + .toLowerCase() + .replace(/-/g, "") + .replace(/[^a-zA-Z0-9]+/g, "|") + .split("|") + .forEach((keyword) => keywordsSet.add(keyword)); + } + } + + const stringValue = Array.from(keywordsSet).join(","); this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue }); } From 3c4eaa1c928e9064b7dab80a0a0aeeafba3f5744 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:56:43 -0400 Subject: [PATCH 03/31] Fix version number for Web (#10400) --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 35ef8056ee5..f8ef4a8030c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.8.0", + "version": "2024.7.3", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index aee0158b816..8b7dfd7fa67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -249,7 +249,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.8.0" + "version": "2024.7.3" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From 3bf820606e4af5f281065a0ed82f5464a5667d0e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:06:13 -0400 Subject: [PATCH 04/31] [deps] Autofill: Update tldts to v6.1.38 (#10393) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 8b541551eed..5f417579081 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.34", + "tldts": "6.1.38", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index 8b7dfd7fa67..81ae338ca39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.34", + "tldts": "6.1.38", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -226,7 +226,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.34", + "tldts": "6.1.38", "zxcvbn": "4.4.2" }, "bin": { @@ -37433,21 +37433,21 @@ "dev": true }, "node_modules/tldts": { - "version": "6.1.34", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.34.tgz", - "integrity": "sha512-ErJIL8DMj1CLBER2aFrjI3IfhtuJD/jEYJA/iQg9wW8dIEPXNl4zcI/SGUihMsM/lP/Jyd8c2ETv6Cwtnk0/RQ==", + "version": "6.1.38", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.38.tgz", + "integrity": "sha512-1onihAOxYDzhsQXl9XMlDQSjdIgMAz3ugom3BdS4K71GbHmNmrRSR5PYFYIBoE4QBB0v1dPqj47D3o/2C9M+KQ==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.34" + "tldts-core": "^6.1.38" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.34", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.34.tgz", - "integrity": "sha512-Hb/jAm14h5x5+gO5Cv5wabKO0pbLlRoryvCC9v0t8OleZ4vXEKego7Mq1id/X1C8Vw1E0QCCQzGdWHkKFtxFrQ==", + "version": "6.1.38", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.38.tgz", + "integrity": "sha512-TKmqyzXCha5k3WFSIW0ofB7W8BkUe1euZ1z9rZLckai5JxqndBt8CuWfusU9EB1qS5ycS+k9zf6Zs0bucKRDkg==", "license": "MIT" }, "node_modules/tmp": { diff --git a/package.json b/package.json index 5e9c95b7deb..3c30f3ec0ad 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.34", + "tldts": "6.1.38", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" From 157f3a5d394d79b8ddd836e58ed8b6a63b57976e Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 5 Aug 2024 10:11:54 -0500 Subject: [PATCH 05/31] [PM-10598] Revert autocomplete="off" removal when qualifying the inline menu for identity fields (#10402) --- .../services/inline-menu-field-qualification.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index e9d5e70c1cd..955334e3fa0 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -286,7 +286,10 @@ export class InlineMenuFieldQualificationService return true; } - return this.keywordsFoundInFieldData(field, this.identityFieldKeywords); + return ( + !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && + this.keywordsFoundInFieldData(field, this.identityFieldKeywords) + ); } /** From 2819ac597f6f7ee25085eea33b0089ad2f1d1330 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Mon, 5 Aug 2024 11:39:08 -0400 Subject: [PATCH 06/31] [BEEEP: PM-10190] Use strict TS checks in CLI service container (#10298) * move cli service-container to new folder * fix imports * add tsconfig and fix type issues in other services * fix more imports in service-container * make ts server happy in service-container * fix actual bugs in cli service-container * fix package json reference path * fix service-container import * update type on cipher service --- apps/cli/src/base-program.ts | 2 +- apps/cli/src/bw.ts | 2 +- apps/cli/src/commands/serve.command.ts | 2 +- apps/cli/src/oss-serve-configurator.ts | 2 +- apps/cli/src/register-oss-programs.ts | 2 +- apps/cli/src/serve.program.ts | 2 +- .../service-container.spec.ts | 0 .../service-container.ts | 102 ++++++++++-------- apps/cli/src/service-container/tsconfig.json | 7 ++ .../bit-cli/src/service-container.ts | 2 +- .../user-decryption-options.service.ts | 8 +- .../policy/policy-api.service.abstraction.ts | 4 +- .../organization/organization.service.ts | 6 +- .../services/policy/policy.service.ts | 2 +- .../src/auth/abstractions/account.service.ts | 2 +- .../src/auth/services/account.service.ts | 23 ++-- .../auth/services/key-connector.service.ts | 3 +- .../src/platform/services/crypto.service.ts | 4 +- .../services/default-environment.service.ts | 2 +- .../src/vault/abstractions/cipher.service.ts | 2 +- .../vault/abstractions/collection.service.ts | 2 +- .../folder/folder.service.abstraction.ts | 4 +- .../src/vault/services/cipher.service.ts | 4 +- .../src/vault/services/collection.service.ts | 2 +- 24 files changed, 110 insertions(+), 81 deletions(-) rename apps/cli/src/{ => service-container}/service-container.spec.ts (100%) rename apps/cli/src/{ => service-container}/service-container.ts (96%) create mode 100644 apps/cli/src/service-container/tsconfig.json diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts index 563b205fa74..f308bdc2deb 100644 --- a/apps/cli/src/base-program.ts +++ b/apps/cli/src/base-program.ts @@ -10,7 +10,7 @@ import { ListResponse } from "./models/response/list.response"; import { MessageResponse } from "./models/response/message.response"; import { StringResponse } from "./models/response/string.response"; import { TemplateResponse } from "./models/response/template.response"; -import { ServiceContainer } from "./service-container"; +import { ServiceContainer } from "./service-container/service-container"; import { CliUtils } from "./utils"; const writeLn = CliUtils.writeLn; diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index e4c46dd9ee9..5e9d3dfbc94 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -3,7 +3,7 @@ import { program } from "commander"; import { OssServeConfigurator } from "./oss-serve-configurator"; import { registerOssPrograms } from "./register-oss-programs"; import { ServeProgram } from "./serve.program"; -import { ServiceContainer } from "./service-container"; +import { ServiceContainer } from "./service-container/service-container"; async function main() { const serviceContainer = new ServiceContainer(); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 05603a3c24a..801f505f1ae 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -7,7 +7,7 @@ import * as koaJson from "koa-json"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OssServeConfigurator } from "../oss-serve-configurator"; -import { ServiceContainer } from "../service-container"; +import { ServiceContainer } from "../service-container/service-container"; export class ServeCommand { constructor( diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 13f50da78b7..8a38f8f1280 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -13,7 +13,7 @@ import { RestoreCommand } from "./commands/restore.command"; import { StatusCommand } from "./commands/status.command"; import { Response } from "./models/response"; import { FileResponse } from "./models/response/file.response"; -import { ServiceContainer } from "./service-container"; +import { ServiceContainer } from "./service-container/service-container"; import { GenerateCommand } from "./tools/generate.command"; import { SendEditCommand, diff --git a/apps/cli/src/register-oss-programs.ts b/apps/cli/src/register-oss-programs.ts index d8aa54118d7..1fc1f0119d2 100644 --- a/apps/cli/src/register-oss-programs.ts +++ b/apps/cli/src/register-oss-programs.ts @@ -1,5 +1,5 @@ import { Program } from "./program"; -import { ServiceContainer } from "./service-container"; +import { ServiceContainer } from "./service-container/service-container"; import { SendProgram } from "./tools/send/send.program"; import { VaultProgram } from "./vault.program"; diff --git a/apps/cli/src/serve.program.ts b/apps/cli/src/serve.program.ts index bbf66661e5b..ef18a3e35ce 100644 --- a/apps/cli/src/serve.program.ts +++ b/apps/cli/src/serve.program.ts @@ -3,7 +3,7 @@ import { program } from "commander"; import { BaseProgram } from "./base-program"; import { ServeCommand } from "./commands/serve.command"; import { OssServeConfigurator } from "./oss-serve-configurator"; -import { ServiceContainer } from "./service-container"; +import { ServiceContainer } from "./service-container/service-container"; import { CliUtils } from "./utils"; const writeLn = CliUtils.writeLn; diff --git a/apps/cli/src/service-container.spec.ts b/apps/cli/src/service-container/service-container.spec.ts similarity index 100% rename from apps/cli/src/service-container.spec.ts rename to apps/cli/src/service-container/service-container.spec.ts diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container/service-container.ts similarity index 96% rename from apps/cli/src/service-container.ts rename to apps/cli/src/service-container/service-container.ts index 8ee99fa2039..d35412ede30 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -44,7 +44,10 @@ import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + AutofillSettingsService, + AutofillSettingsServiceAbstraction, +} from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, @@ -147,18 +150,18 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; -import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; -import { ConsoleLogService } from "./platform/services/console-log.service"; -import { I18nService } from "./platform/services/i18n.service"; -import { LowdbStorageService } from "./platform/services/lowdb-storage.service"; -import { NodeApiService } from "./platform/services/node-api.service"; -import { NodeEnvSecureStorageService } from "./platform/services/node-env-secure-storage.service"; +import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service"; +import { ConsoleLogService } from "../platform/services/console-log.service"; +import { I18nService } from "../platform/services/i18n.service"; +import { LowdbStorageService } from "../platform/services/lowdb-storage.service"; +import { NodeApiService } from "../platform/services/node-api.service"; +import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service"; // Polyfills global.DOMParser = new jsdom.JSDOM().window.DOMParser; // eslint-disable-next-line -const packageJson = require("../package.json"); +const packageJson = require("../../package.json"); /** * Instantiates services and makes them available for dependency injection. @@ -254,13 +257,13 @@ export class ServiceContainer { } else if (process.env.BITWARDENCLI_APPDATA_DIR) { p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR); } else if (process.platform === "darwin") { - p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI"); + p = path.join(process.env.HOME ?? "", "Library/Application Support/Bitwarden CLI"); } else if (process.platform === "win32") { - p = path.join(process.env.APPDATA, "Bitwarden CLI"); + p = path.join(process.env.APPDATA ?? "", "Bitwarden CLI"); } else if (process.env.XDG_CONFIG_HOME) { p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI"); } else { - p = path.join(process.env.HOME, ".config/Bitwarden CLI"); + p = path.join(process.env.HOME ?? "", ".config/Bitwarden CLI"); } const logoutCallback = async () => await this.logout(); @@ -452,8 +455,6 @@ export class ServiceContainer { customUserAgent, ); - this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); - this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); @@ -524,6 +525,40 @@ export class ServiceContainer { this.stateProvider, ); + this.authRequestService = new AuthRequestService( + this.appIdService, + this.accountService, + this.masterPasswordService, + this.cryptoService, + this.apiService, + this.stateProvider, + ); + + this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( + this.stateProvider, + ); + + this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); + + this.authService = new AuthService( + this.accountService, + this.messagingService, + this.cryptoService, + this.apiService, + this.stateService, + this.tokenService, + ); + + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); + + this.configService = new DefaultConfigService( + this.configApiService, + this.environmentService, + this.logService, + this.stateProvider, + this.authService, + ); + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, @@ -541,20 +576,6 @@ export class ServiceContainer { this.configService, ); - this.authRequestService = new AuthRequestService( - this.appIdService, - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.stateProvider, - ); - - this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.stateProvider, - ); - - this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, @@ -583,23 +604,10 @@ export class ServiceContainer { this.taskSchedulerService, ); - this.authService = new AuthService( - this.accountService, - this.messagingService, - this.cryptoService, - this.apiService, - this.stateService, - this.tokenService, - ); - - this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - - this.configService = new DefaultConfigService( - this.configApiService, - this.environmentService, - this.logService, + // FIXME: CLI does not support autofill + this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, - this.authService, + this.policyService, ); this.cipherService = new CipherService( @@ -661,7 +669,7 @@ export class ServiceContainer { this.taskSchedulerService, this.logService, lockedCallback, - null, + undefined, ); this.avatarService = new AvatarService(this.apiService, this.stateProvider); @@ -752,6 +760,8 @@ export class ServiceContainer { this.accountService, ); + this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); + this.providerApiService = new ProviderApiService(this.apiService); } @@ -774,7 +784,7 @@ export class ServiceContainer { await this.stateService.clean(); await this.accountService.clean(userId); await this.accountService.switchAccount(null); - process.env.BW_SESSION = null; + process.env.BW_SESSION = undefined; } async init() { @@ -790,7 +800,7 @@ export class ServiceContainer { this.twoFactorService.init(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - if (activeAccount) { + if (activeAccount?.id) { await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); } diff --git a/apps/cli/src/service-container/tsconfig.json b/apps/cli/src/service-container/tsconfig.json new file mode 100644 index 00000000000..ee24d230ae2 --- /dev/null +++ b/apps/cli/src/service-container/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "strictNullChecks": true, + "strictPropertyInitialization": true + } +} diff --git a/bitwarden_license/bit-cli/src/service-container.ts b/bitwarden_license/bit-cli/src/service-container.ts index 995e14531d7..0238829a132 100644 --- a/bitwarden_license/bit-cli/src/service-container.ts +++ b/bitwarden_license/bit-cli/src/service-container.ts @@ -2,7 +2,7 @@ import { OrganizationAuthRequestService, OrganizationAuthRequestApiService, } from "@bitwarden/bit-common/admin-console/auth-requests"; -import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container"; +import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container/service-container"; /** * Instantiates services and makes them available for dependency injection. diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts index 6651ffd9e51..e4200c759e8 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -1,4 +1,4 @@ -import { map } from "rxjs"; +import { Observable, map } from "rxjs"; import { ActiveUserState, @@ -25,8 +25,8 @@ export class UserDecryptionOptionsService { private userDecryptionOptionsState: ActiveUserState; - userDecryptionOptions$; - hasMasterPassword$; + userDecryptionOptions$: Observable; + hasMasterPassword$: Observable; constructor(private stateProvider: StateProvider) { this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS); @@ -37,7 +37,7 @@ export class UserDecryptionOptionsService ); } - userDecryptionOptionsById$(userId: UserId) { + userDecryptionOptionsById$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$; } diff --git a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts index c601aad0607..c7fec8813c7 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts @@ -16,6 +16,8 @@ export class PolicyApiServiceAbstraction { organizationUserId: string, ) => Promise; - getMasterPasswordPolicyOptsForOrgUser: (orgId: string) => Promise; + getMasterPasswordPolicyOptsForOrgUser: ( + orgId: string, + ) => Promise; putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise; } diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts index 7013863c5cd..d8fe18dc5cb 100644 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ b/libs/common/src/admin-console/services/organization/organization.service.ts @@ -64,8 +64,10 @@ function mapToSingleOrganization(organizationId: string) { } export class OrganizationService implements InternalOrganizationServiceAbstraction { - organizations$ = this.getOrganizationsFromState$(); - memberOrganizations$ = this.organizations$.pipe(mapToExcludeProviderOrganizations()); + organizations$: Observable = this.getOrganizationsFromState$(); + memberOrganizations$: Observable = this.organizations$.pipe( + mapToExcludeProviderOrganizations(), + ); constructor(private stateProvider: StateProvider) {} diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 7ec525f9606..2287ef9b4f4 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -32,7 +32,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction { private organizationService: OrganizationService, ) {} - get$(policyType: PolicyType) { + get$(policyType: PolicyType): Observable { const filteredPolicies$ = this.activeUserPolicies$.pipe( map((policies) => policies.filter((p) => p.type === policyType)), ); diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index b7fd6d9bb93..849abc65f66 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -72,7 +72,7 @@ export abstract class AccountService { * Updates the `activeAccount$` observable with the new active account. * @param userId */ - abstract switchAccount(userId: UserId): Promise; + abstract switchAccount(userId: UserId | null): Promise; /** * Cleans personal information for the given account from the `accounts$` observable. Does not remove the userId from the observable. * diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 8af06b89e73..dceec2cbf13 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -1,4 +1,11 @@ -import { combineLatestWith, map, distinctUntilChanged, shareReplay, combineLatest } from "rxjs"; +import { + combineLatestWith, + map, + distinctUntilChanged, + shareReplay, + combineLatest, + Observable, +} from "rxjs"; import { AccountInfo, @@ -42,11 +49,11 @@ export class AccountServiceImplementation implements InternalAccountService { private accountsState: GlobalState>; private activeAccountIdState: GlobalState; - accounts$; - activeAccount$; - accountActivity$; - sortedUserIds$; - nextUpAccount$; + accounts$: Observable>; + activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; + accountActivity$: Observable>; + sortedUserIds$: Observable; + nextUpAccount$: Observable<{ id: UserId } & AccountInfo>; constructor( private messagingService: MessagingService, @@ -61,7 +68,7 @@ export class AccountServiceImplementation implements InternalAccountService { ); this.activeAccount$ = this.activeAccountIdState.state$.pipe( combineLatestWith(this.accounts$), - map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)), + map(([id, accounts]) => (id ? { id, ...(accounts[id] as AccountInfo) } : undefined)), distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)), shareReplay({ bufferSize: 1, refCount: false }), ); @@ -118,7 +125,7 @@ export class AccountServiceImplementation implements InternalAccountService { await this.removeAccountActivity(userId); } - async switchAccount(userId: UserId): Promise { + async switchAccount(userId: UserId | null): Promise { let updateActivity = false; await this.activeAccountIdState.update( (_, accounts) => { diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index a70bbb6ffb0..8f204e557ed 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -5,6 +5,7 @@ import { LogoutReason } from "@bitwarden/auth/common"; import { ApiService } from "../../abstractions/api.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; +import { Organization } from "../../admin-console/models/domain/organization"; import { KeysRequest } from "../../models/request/keys.request"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; @@ -114,7 +115,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } } - async getManagingOrganization() { + async getManagingOrganization(): Promise { const orgs = await this.organizationService.getAll(); return orgs.find( (o) => diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 4d8a447576f..b139775ea48 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -888,7 +888,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.encryptService.decryptToBytes(encBuffer, key); } - userKey$(userId: UserId) { + userKey$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_KEY).state$; } @@ -1013,7 +1013,7 @@ export class CryptoService implements CryptoServiceAbstraction { ); } - orgKeys$(userId: UserId) { + orgKeys$(userId: UserId): Observable | null> { return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys)); } diff --git a/libs/common/src/platform/services/default-environment.service.ts b/libs/common/src/platform/services/default-environment.service.ts index 59956ede7ae..97f084d80f3 100644 --- a/libs/common/src/platform/services/default-environment.service.ts +++ b/libs/common/src/platform/services/default-environment.service.ts @@ -269,7 +269,7 @@ export class DefaultEnvironmentService implements EnvironmentService { } } - async getEnvironment(userId?: UserId) { + async getEnvironment(userId?: UserId): Promise { if (userId == null) { return await firstValueFrom(this.environment$); } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index ad4a08c2fed..83bdf016ed1 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -129,7 +129,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise>; replace: (ciphers: { [id: string]: CipherData }) => Promise; - clear: (userId: string) => Promise; + clear: (userId?: string) => Promise; moveManyWithServer: (ids: string[], folderId: string) => Promise; delete: (id: string | string[]) => Promise; deleteWithServer: (id: string, asAdmin?: boolean) => Promise; diff --git a/libs/common/src/vault/abstractions/collection.service.ts b/libs/common/src/vault/abstractions/collection.service.ts index 87ce8edf435..0c206139633 100644 --- a/libs/common/src/vault/abstractions/collection.service.ts +++ b/libs/common/src/vault/abstractions/collection.service.ts @@ -23,6 +23,6 @@ export abstract class CollectionService { getNested: (id: string) => Promise>; upsert: (collection: CollectionData | CollectionData[]) => Promise; replace: (collections: { [id: string]: CollectionData }) => Promise; - clear: (userId: string) => Promise; + clear: (userId?: string) => Promise; delete: (id: string | string[]) => Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index c22eb1febb9..71b8089fa6f 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -17,7 +17,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; - getDecrypted$: (id: string) => Observable; + getDecrypted$: (id: string) => Observable; getAllFromState: () => Promise; /** * @deprecated Only use in CLI! @@ -46,6 +46,6 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; replace: (folders: { [id: string]: FolderData }) => Promise; - clear: (userId: string) => Promise; + clear: (userId?: string) => Promise; delete: (id: string | string[]) => Promise; } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 374f66c42e6..3eeac75db09 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -571,7 +571,7 @@ export class CipherService implements CipherServiceAbstraction { return this.sortedCiphersCache.getNext(cacheKey); } - async getNextIdentityCipher() { + async getNextIdentityCipher(): Promise { const cacheKey = "identityCiphers"; if (!this.sortedCiphersCache.isCached(cacheKey)) { @@ -926,7 +926,7 @@ export class CipherService implements CipherServiceAbstraction { return updatedCiphers; } - async clear(userId?: UserId): Promise { + async clear(userId?: UserId): Promise { userId ??= await firstValueFrom(this.stateProvider.activeUserId$); await this.clearEncryptedCiphersState(userId); await this.clearCache(userId); diff --git a/libs/common/src/vault/services/collection.service.ts b/libs/common/src/vault/services/collection.service.ts index 096edb77f99..47063aa29dc 100644 --- a/libs/common/src/vault/services/collection.service.ts +++ b/libs/common/src/vault/services/collection.service.ts @@ -188,7 +188,7 @@ export class CollectionService implements CollectionServiceAbstraction { await this.encryptedCollectionDataState.update(() => collections); } - async clear(userId?: UserId): Promise { + async clear(userId?: UserId): Promise { if (userId == null) { await this.encryptedCollectionDataState.update(() => null); await this.decryptedCollectionDataState.forceValue(null); From 7e3358d4ee7873bdb9ffb499ffbc6fb4909e2053 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 5 Aug 2024 12:20:13 -0400 Subject: [PATCH 07/31] move fido2 popup components from vault to autofill (#10381) --- .../popup}/fido2/fido2-cipher-row.component.html | 0 .../popup}/fido2/fido2-cipher-row.component.ts | 0 .../popup}/fido2/fido2-use-browser-link.component.html | 0 .../popup}/fido2/fido2-use-browser-link.component.ts | 4 ++-- .../popup}/fido2/fido2.component.html | 0 .../popup}/fido2/fido2.component.ts | 8 ++++---- apps/browser/src/popup/app-routing.module.ts | 2 +- apps/browser/src/popup/app.module.ts | 6 +++--- 8 files changed, 10 insertions(+), 10 deletions(-) rename apps/browser/src/{vault/popup/components => autofill/popup}/fido2/fido2-cipher-row.component.html (100%) rename apps/browser/src/{vault/popup/components => autofill/popup}/fido2/fido2-cipher-row.component.ts (100%) rename apps/browser/src/{vault/popup/components => autofill/popup}/fido2/fido2-use-browser-link.component.html (100%) rename apps/browser/src/{vault/popup/components => autofill/popup}/fido2/fido2-use-browser-link.component.ts (94%) rename apps/browser/src/{vault/popup/components => autofill/popup}/fido2/fido2.component.html (100%) rename apps/browser/src/{vault/popup/components => autofill/popup}/fido2/fido2.component.ts (97%) diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html rename to apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts rename to apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.html rename to apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.html diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts similarity index 94% rename from apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts rename to apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index 9c69d76228f..b97c4102fed 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -9,8 +9,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service"; -import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data"; +import { BrowserFido2UserInterfaceSession } from "../../../vault/fido2/browser-fido2-user-interface.service"; +import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; @Component({ selector: "app-fido2-use-browser-link", diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/fido2/fido2.component.html rename to apps/browser/src/autofill/popup/fido2/fido2.component.html diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts similarity index 97% rename from apps/browser/src/vault/popup/components/fido2/fido2.component.ts rename to apps/browser/src/autofill/popup/fido2/fido2.component.ts index c0389f5afdd..d720a5240f7 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -29,13 +29,13 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note. import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -import { ZonedMessageListenerService } from "../../../../platform/browser/zoned-message-listener.service"; +import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; import { BrowserFido2Message, BrowserFido2UserInterfaceSession, -} from "../../../fido2/browser-fido2-user-interface.service"; -import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service"; -import { VaultPopoutType } from "../../utils/vault-popout-window"; +} from "../../../vault/fido2/browser-fido2-user-interface.service"; +import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; +import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; interface ViewData { message: BrowserFido2Message; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 47d451cc016..e556e459287 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -39,6 +39,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; @@ -63,7 +64,6 @@ import { ImportBrowserV2Component } from "../tools/popup/settings/import/import- import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { SettingsComponent } from "../tools/popup/settings/settings.component"; -import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; import { CollectionsComponent } from "../vault/popup/components/vault/collections.component"; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 2165cf6fce6..56ddd3c6ba3 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,6 +35,9 @@ import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; +import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; +import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; @@ -58,9 +61,6 @@ import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; -import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component"; -import { Fido2UseBrowserLinkComponent } from "../vault/popup/components/fido2/fido2-use-browser-link.component"; -import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; From fdcf1c7ea25ea2e6562fb25b8ba8b4f77c2d5c97 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 5 Aug 2024 11:32:29 -0500 Subject: [PATCH 08/31] [PM-10554] Inline menu glitches on single form fields that are wrapped within an iframe (#10384) * [PM-10554] Inline menu glitches on single form fields that are wrapped within inline menu * [PM-10554] Adding jest tests to validate expected behavior * [PM-10554] Adding jest tests to validate expected behavior --- .../abstractions/overlay.background.ts | 2 +- .../background/overlay.background.spec.ts | 30 ++++++++++++++++--- .../autofill/background/overlay.background.ts | 27 ++++++++++++++--- .../autofill-overlay-content.service.spec.ts | 13 ++++++++ .../autofill-overlay-content.service.ts | 11 +++++-- 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 401196b256f..57eb391e4d1 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -161,7 +161,7 @@ export type OverlayBackgroundExtensionMessageHandlers = { triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateIsFieldCurrentlyFocused: ({ message }: BackgroundMessageParam) => void; + updateIsFieldCurrentlyFocused: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; checkIsFieldCurrentlyFocused: () => boolean; updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; checkIsFieldCurrentlyFilling: () => boolean; diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 783010ee04d..725c91510d8 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -526,10 +526,13 @@ describe("OverlayBackground", () => { }); it("skips updating the position of either inline menu element if a field is not currently focused", async () => { - sendMockExtensionMessage({ - command: "updateIsFieldCurrentlyFocused", - isFieldCurrentlyFocused: false, - }); + sendMockExtensionMessage( + { + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }, + mock({ frameId: 20 }), + ); sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); await flushUpdateInlineMenuPromises(); @@ -1362,6 +1365,25 @@ describe("OverlayBackground", () => { }); }); + describe("updateIsFieldCurrentlyFocused message handler", () => { + it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 1 }, frameId: 10 }), + ); + overlayBackground["isFieldCurrentlyFocused"] = true; + + sendMockExtensionMessage( + { command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false }, + mock({ tab: { id: 1 }, frameId: 20 }), + ); + await flushPromises(); + + expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true); + }); + }); + describe("checkIsFieldCurrentlyFocused message handler", () => { it("returns true when a form field is currently focused", async () => { sendMockExtensionMessage({ diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 029d2d69ac6..ed70a14c4aa 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -97,7 +97,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { checkIsInlineMenuCiphersPopulated: ({ sender }) => this.checkIsInlineMenuCiphersPopulated(sender), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), - updateIsFieldCurrentlyFocused: ({ message }) => this.updateIsFieldCurrentlyFocused(message), + updateIsFieldCurrentlyFocused: ({ message, sender }) => + this.updateIsFieldCurrentlyFocused(message, sender), checkIsFieldCurrentlyFocused: () => this.checkIsFieldCurrentlyFocused(), updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), @@ -1090,7 +1091,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { { focusedFieldData }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if (this.focusedFieldData?.frameId && this.focusedFieldData.frameId !== sender.frameId) { + if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { void BrowserApi.tabSendMessage( sender.tab, { command: "unsetMostRecentlyFocusedField" }, @@ -1100,6 +1101,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const previousFocusedFieldData = this.focusedFieldData; this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; + this.isFieldCurrentlyFocused = true; const accountCreationFieldBlurred = previousFocusedFieldData?.showInlineMenuAccountCreation && @@ -1558,8 +1560,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Updates the property that identifies if a form field set up for the inline menu is currently focused. * * @param message - The message received from the web page + * @param sender - The sender of the port message */ - private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) { + private updateIsFieldCurrentlyFocused( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { + return; + } + this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused; } @@ -1651,7 +1661,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return false; } - if (this.focusedFieldData?.frameId === sender.frameId) { + if (this.senderFrameHasFocusedField(sender)) { return true; } @@ -1676,6 +1686,15 @@ export class OverlayBackground implements OverlayBackgroundInterface { return sender.tab.id === this.focusedFieldData?.tabId; } + /** + * Identifies if the sender frame is the same as the focused field's frame. + * + * @param sender - The sender of the message + */ + private senderFrameHasFocusedField(sender: chrome.runtime.MessageSender) { + return sender.frameId === this.focusedFieldData?.frameId; + } + /** * Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu * if the focused field is within the viewport. diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 6145dbfdc0e..15eeff75cd9 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -1986,6 +1986,19 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldFocusSpy).not.toHaveBeenCalled(); expect(nextFocusableElement.focus).toHaveBeenCalled(); }); + + it("focuses the most recently focused input field if no other tabbable elements are found", async () => { + autofillOverlayContentService["focusableElements"] = []; + findTabsSpy.mockReturnValue([]); + + sendMockExtensionMessage({ + command: "redirectAutofillInlineMenuFocusOut", + data: { direction: RedirectFocusDirection.Next }, + }); + await flushPromises(); + + expect(autofillFieldFocusSpy).toHaveBeenCalled(); + }); }); describe("updateAutofillInlineMenuVisibility message handler", () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 0881ecba1ef..9a45403edcb 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -338,7 +338,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ const indexOffset = direction === RedirectFocusDirection.Previous ? -1 : 1; const redirectFocusElement = this.focusableElements[focusedElementIndex + indexOffset]; - redirectFocusElement?.focus(); + if (redirectFocusElement) { + redirectFocusElement.focus(); + return; + } + + this.focusMostRecentlyFocusedField(); } /** @@ -1418,8 +1423,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ focusedFieldRectsTop + this.focusedFieldData?.focusedFieldRects?.height; const viewportHeight = globalThis.innerHeight + globalThis.scrollY; return ( - focusedFieldRectsTop && - focusedFieldRectsTop > 0 && + !globalThis.isNaN(focusedFieldRectsTop) && + focusedFieldRectsTop >= 0 && focusedFieldRectsTop < viewportHeight && focusedFieldRectsBottom < viewportHeight ); From cecfbaeaad35ccd3573b0501787d1a25ae9e4078 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 5 Aug 2024 12:03:17 -0500 Subject: [PATCH 09/31] [PM-10552] Values input between separate iframes are not populated when adding a cipher from the inline menu (#10401) * [PM-10554] Inline menu glitches on single form fields that are wrapped within inline menu * [PM-10554] Adding jest tests to validate expected behavior * [PM-10554] Adding jest tests to validate expected behavior * [PM-10552] Vaules input between separate iframes are not populated in add-edit cipher popout * [PM-10552] Working through issues found when attempting to add ciphers within iframes that trigger a blur event * [PM-10552] Working through issues found when attempting to add ciphers within iframes that trigger a blur event * [PM-10552] Fixing broken jest tests due to implementation changes * [PM-10552] Implementing jest tests to validate behavior within OverlayBackground --- .../abstractions/overlay.background.ts | 4 + .../background/overlay.background.spec.ts | 266 +++++++++++++++++- .../autofill/background/overlay.background.ts | 259 ++++++++++++++--- .../autofill-overlay-content.service.spec.ts | 4 +- .../autofill-overlay-content.service.ts | 14 +- 5 files changed, 482 insertions(+), 65 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 57eb391e4d1..8122f5c4ed9 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -95,6 +95,10 @@ export type OverlayAddNewItemMessage = { identity?: NewIdentityCipherData; }; +export type CurrentAddNewItemData = OverlayAddNewItemMessage & { + sender: chrome.runtime.MessageSender; +}; + export type CloseInlineMenuMessage = { forceCloseInlineMenu?: boolean; overlayElement?: string; diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 725c91510d8..fe118868628 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -176,8 +176,12 @@ describe("OverlayBackground", () => { parentFrameId: getFrameCounter, }); }); - tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage"); - tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData"); + tabsSendMessageSpy = jest + .spyOn(BrowserApi, "tabSendMessage") + .mockImplementation(() => Promise.resolve()); + tabSendMessageDataSpy = jest + .spyOn(BrowserApi, "tabSendMessageData") + .mockImplementation(() => Promise.resolve()); sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); getTabSpy = jest.spyOn(BrowserApi, "getTab"); @@ -838,7 +842,7 @@ describe("OverlayBackground", () => { it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id }); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); @@ -857,7 +861,7 @@ describe("OverlayBackground", () => { image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", imageEnabled: true, }, - id: "inline-menu-cipher-1", + id: "inline-menu-cipher-0", login: { username: "username-1", }, @@ -1119,10 +1123,12 @@ describe("OverlayBackground", () => { let openAddEditVaultItemPopoutSpy: jest.SpyInstance; beforeEach(() => { + jest.useFakeTimers(); sender = mock({ tab: { id: 1 } }); openAddEditVaultItemPopoutSpy = jest .spyOn(overlayBackground as any, "openAddEditVaultItemPopout") .mockImplementation(); + overlayBackground["currentAddNewItemData"] = { sender, addNewCipherType: CipherType.Login }; }); it("will not open the add edit popout window if the message does not have a login cipher provided", () => { @@ -1132,6 +1138,28 @@ describe("OverlayBackground", () => { expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled(); }); + it("resets the currentAddNewItemData to null when a cipher view is not successfully created", async () => { + jest.spyOn(overlayBackground as any, "buildLoginCipherView").mockReturnValue(null); + + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Login, + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(overlayBackground["currentAddNewItemData"]).toBeNull(); + }); + it("will open the add edit popout window after creating a new cipher", async () => { sendMockExtensionMessage( { @@ -1146,6 +1174,7 @@ describe("OverlayBackground", () => { }, sender, ); + jest.advanceTimersByTime(100); await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); @@ -1154,6 +1183,8 @@ describe("OverlayBackground", () => { }); it("creates a new card cipher", async () => { + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card; + sendMockExtensionMessage( { command: "autofillOverlayAddNewVaultItem", @@ -1169,6 +1200,7 @@ describe("OverlayBackground", () => { }, sender, ); + jest.advanceTimersByTime(100); await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); @@ -1177,6 +1209,10 @@ describe("OverlayBackground", () => { }); describe("creating a new identity cipher", () => { + beforeEach(() => { + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity; + }); + it("populates an identity cipher view and creates it", async () => { sendMockExtensionMessage( { @@ -1203,6 +1239,7 @@ describe("OverlayBackground", () => { }, sender, ); + jest.advanceTimersByTime(100); await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); @@ -1223,6 +1260,7 @@ describe("OverlayBackground", () => { }, sender, ); + jest.advanceTimersByTime(100); await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); @@ -1241,6 +1279,7 @@ describe("OverlayBackground", () => { }, sender, ); + jest.advanceTimersByTime(100); await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); @@ -1259,11 +1298,173 @@ describe("OverlayBackground", () => { }, sender, ); + jest.advanceTimersByTime(100); await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); }); }); + + describe("pulling cipher data from multiple frames of a tab", () => { + let subFrameSender: MockProxy; + const command = "autofillOverlayAddNewVaultItem"; + + beforeEach(() => { + subFrameSender = mock({ tab: sender.tab, frameId: 2 }); + }); + + it("combines the login cipher data from all frames", async () => { + const buildLoginCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildLoginCipherView", + ); + const addNewCipherType = CipherType.Login; + const loginCipherData = { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "", + }; + const subFrameLoginCipherData = { + uri: "https://tacos.com", + hostname: "tacos.com", + username: "", + password: "password", + }; + + sendMockExtensionMessage({ command, addNewCipherType, login: loginCipherData }, sender); + sendMockExtensionMessage( + { command, addNewCipherType, login: subFrameLoginCipherData }, + subFrameSender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildLoginCipherViewSpy).toHaveBeenCalledWith({ + uri: "https://tacos.com", + hostname: "tacos.com", + username: "username", + password: "password", + }); + }); + + it("combines the card cipher data from all frames", async () => { + const buildCardCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildCardCipherView", + ); + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card; + const addNewCipherType = CipherType.Card; + const cardCipherData = { + cardholderName: "cardholderName", + number: "", + expirationMonth: "", + expirationYear: "", + expirationDate: "12/25", + cvv: "123", + }; + const subFrameCardCipherData = { + cardholderName: "", + number: "4242424242424242", + expirationMonth: "12", + expirationYear: "2025", + expirationDate: "", + cvv: "", + }; + + sendMockExtensionMessage({ command, addNewCipherType, card: cardCipherData }, sender); + sendMockExtensionMessage( + { command, addNewCipherType, card: subFrameCardCipherData }, + subFrameSender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildCardCipherViewSpy).toHaveBeenCalledWith({ + cardholderName: "cardholderName", + number: "4242424242424242", + expirationMonth: "12", + expirationYear: "2025", + expirationDate: "12/25", + cvv: "123", + }); + }); + + it("combines the identity cipher data from all frames", async () => { + const buildIdentityCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildIdentityCipherView", + ); + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity; + const addNewCipherType = CipherType.Identity; + const identityCipherData = { + title: "title", + firstName: "firstName", + middleName: "middleName", + lastName: "", + fullName: "", + address1: "address1", + address2: "address2", + address3: "address3", + city: "city", + state: "state", + postalCode: "postalCode", + country: "country", + company: "company", + phone: "phone", + email: "email", + username: "username", + }; + const subFrameIdentityCipherData = { + title: "", + firstName: "", + middleName: "", + lastName: "lastName", + fullName: "fullName", + address1: "", + address2: "", + address3: "", + city: "", + state: "", + postalCode: "", + country: "", + company: "", + phone: "", + email: "", + username: "", + }; + + sendMockExtensionMessage( + { command, addNewCipherType, identity: identityCipherData }, + sender, + ); + sendMockExtensionMessage( + { command, addNewCipherType, identity: subFrameIdentityCipherData }, + subFrameSender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildIdentityCipherViewSpy).toHaveBeenCalledWith({ + title: "title", + firstName: "firstName", + middleName: "middleName", + lastName: "lastName", + fullName: "fullName", + address1: "address1", + address2: "address2", + address3: "address3", + city: "city", + state: "state", + postalCode: "postalCode", + country: "country", + company: "company", + phone: "phone", + email: "email", + username: "username", + }); + }); + }); }); describe("checkIsInlineMenuCiphersPopulated message handler", () => { @@ -1363,6 +1564,51 @@ describe("OverlayBackground", () => { showInlineMenuAccountCreation: true, }); }); + + it("triggers an update of the inline menu ciphers when the new focused field's cipher type does not equal the previous focused field's cipher type", async () => { + const updateOverlayCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + const tab = createChromeTabMock({ id: 2 }); + const sender = mock({ tab, frameId: 100 }); + const focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + filledByCipherType: CipherType.Login, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + const newFocusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + filledByCipherType: CipherType.Card, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData: newFocusedFieldData }, + sender, + ); + await flushPromises(); + + expect(updateOverlayCiphersSpy).toHaveBeenCalled(); + }); + }); + + describe("updateIsFieldCurrentlyFocused message handler", () => { + it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 1 }, frameId: 10 }), + ); + overlayBackground["isFieldCurrentlyFocused"] = true; + + sendMockExtensionMessage( + { command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false }, + mock({ tab: { id: 1 }, frameId: 20 }), + ); + await flushPromises(); + + expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true); + }); }); describe("updateIsFieldCurrentlyFocused message handler", () => { @@ -1841,7 +2087,6 @@ describe("OverlayBackground", () => { overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([ [focusedFieldData.frameId, null], ]); - tabsSendMessageSpy.mockImplementation(); jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent"); sendMockExtensionMessage( @@ -2090,7 +2335,6 @@ describe("OverlayBackground", () => { describe("autofillInlineMenuButtonClicked message handler", () => { it("opens the unlock vault popout if the user auth status is not unlocked", async () => { activeAccountStatusMock$.next(AuthenticationStatus.Locked); - tabsSendMessageSpy.mockImplementation(); sendPortMessage(buttonMessageConnectorSpy, { command: "autofillInlineMenuButtonClicked", @@ -2291,7 +2535,6 @@ describe("OverlayBackground", () => { describe("unlockVault message handler", () => { it("opens the unlock vault popout", async () => { activeAccountStatusMock$.next(AuthenticationStatus.Locked); - tabsSendMessageSpy.mockImplementation(); sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey }); await flushPromises(); @@ -2443,11 +2686,10 @@ describe("OverlayBackground", () => { }); await flushPromises(); - expect(tabsSendMessageSpy).toHaveBeenCalledWith( - sender.tab, - { command: "addNewVaultItemFromOverlay", addNewCipherType: CipherType.Login }, - { frameId: sender.frameId }, - ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "addNewVaultItemFromOverlay", + addNewCipherType: CipherType.Login, + }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index ed70a14c4aa..8c4dac56d50 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -42,6 +42,7 @@ import { generateRandomChars } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { CloseInlineMenuMessage, + CurrentAddNewItemData, FocusedFieldData, InlineMenuButtonPortMessageHandlers, InlineMenuCipherData, @@ -83,6 +84,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { private cancelUpdateInlineMenuPositionSubject = new Subject(); private repositionInlineMenuSubject = new Subject(); private rebuildSubFrameOffsetsSubject = new Subject(); + private addNewVaultItemSubject = new Subject(); + private currentAddNewItemData: CurrentAddNewItemData; private focusedFieldData: FocusedFieldData; private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFilling: boolean = false; @@ -187,6 +190,14 @@ export class OverlayBackground implements OverlayBackgroundInterface { switchMap((sender) => this.rebuildSubFrameOffsets(sender)), ) .subscribe(); + this.addNewVaultItemSubject + .pipe( + debounceTime(100), + switchMap((addNewItemData) => + this.buildCipherAndOpenAddEditVaultItemPopout(addNewItemData), + ), + ) + .subscribe(); // Debounce used to update inline menu position merge( @@ -231,14 +242,14 @@ export class OverlayBackground implements OverlayBackgroundInterface { const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); if (authStatus !== AuthenticationStatus.Unlocked) { if (this.focusedFieldData) { - void this.closeInlineMenuAfterCiphersUpdate(); + this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); } return; } const currentTab = await BrowserApi.getTabFromCurrentWindowId(); if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) { - void this.closeInlineMenuAfterCiphersUpdate(); + this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); } this.inlineMenuCiphers = new Map(); @@ -319,7 +330,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private async getInlineMenuCipherData(): Promise { const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers); - let inlineMenuCipherData: InlineMenuCipherData[] = []; + let inlineMenuCipherData: InlineMenuCipherData[]; if (this.showInlineMenuAccountCreation()) { inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers( @@ -527,10 +538,14 @@ export class OverlayBackground implements OverlayBackgroundInterface { }; if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) { - void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url); - void BrowserApi.tabSendMessage(pageDetails.tab, { + this.buildSubFrameOffsets( + pageDetails.tab, + pageDetails.frameId, + pageDetails.details.url, + ).catch((error) => this.logService.error(error)); + BrowserApi.tabSendMessage(pageDetails.tab, { command: "setupRebuildSubFrameOffsetsListeners", - }); + }).catch((error) => this.logService.error(error)); } const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; @@ -620,11 +635,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (!subFrameOffset) { subFrameOffsetsForTab.set(frameId, null); - void BrowserApi.tabSendMessage( + BrowserApi.tabSendMessage( tab, { command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId }, { frameId }, - ); + ).catch((error) => this.logService.error(error)); return; } @@ -656,11 +671,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { frameId, ); - void BrowserApi.tabSendMessage( + BrowserApi.tabSendMessage( tab, { command: "destroyAutofillInlineMenuListeners" }, { frameId }, - ); + ).catch((error) => this.logService.error(error)); } /** @@ -696,13 +711,15 @@ export class OverlayBackground implements OverlayBackgroundInterface { } if (!this.checkIsInlineMenuButtonVisible()) { - void this.toggleInlineMenuHidden( + this.toggleInlineMenuHidden( { isInlineMenuHidden: false, setTransparentInlineMenu: true }, sender, - ); + ).catch((error) => this.logService.error(error)); } - void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender); + this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender).catch( + (error) => this.logService.error(error), + ); const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage( sender.tab, @@ -722,7 +739,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender); + this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender).catch( + (error) => this.logService.error(error), + ); } /** @@ -807,7 +826,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { const command = "closeAutofillInlineMenu"; const sendOptions = { frameId: 0 }; if (forceCloseInlineMenu) { - void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch( + (error) => this.logService.error(error), + ); this.isInlineMenuButtonVisible = false; this.isInlineMenuListVisible = false; return; @@ -818,11 +839,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { } if (this.isFieldCurrentlyFilling) { - void BrowserApi.tabSendMessage( + BrowserApi.tabSendMessage( sender.tab, { command, overlayElement: AutofillOverlayElement.List }, sendOptions, - ); + ).catch((error) => this.logService.error(error)); this.isInlineMenuListVisible = false; return; } @@ -840,7 +861,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.isInlineMenuListVisible = false; } - void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch((error) => + this.logService.error(error), + ); } /** @@ -1092,11 +1115,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { sender: chrome.runtime.MessageSender, ) { if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { - void BrowserApi.tabSendMessage( + BrowserApi.tabSendMessage( sender.tab, { command: "unsetMostRecentlyFocusedField" }, { frameId: this.focusedFieldData.frameId }, - ); + ).catch((error) => this.logService.error(error)); } const previousFocusedFieldData = this.focusedFieldData; @@ -1108,7 +1131,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { !this.focusedFieldData.showInlineMenuAccountCreation; if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) { - void this.updateIdentityCiphersOnLoginField(previousFocusedFieldData); + this.updateIdentityCiphersOnLoginField(previousFocusedFieldData).catch((error) => + this.logService.error(error), + ); + return; + } + + if (previousFocusedFieldData?.filledByCipherType !== focusedFieldData?.filledByCipherType) { + const updateAllCipherTypes = focusedFieldData.filledByCipherType !== CipherType.Login; + this.updateOverlayCiphers(updateAllCipherTypes).catch((error) => + this.logService.error(error), + ); } } @@ -1355,9 +1388,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - void BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", { + BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", { direction, - }); + }).catch((error) => this.logService.error(error)); } /** @@ -1375,13 +1408,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - void BrowserApi.tabSendMessage( - sender.tab, - { command: "addNewVaultItemFromOverlay", addNewCipherType }, - { - frameId: this.focusedFieldData.frameId || 0, - }, - ); + this.currentAddNewItemData = { addNewCipherType, sender }; + BrowserApi.tabSendMessage(sender.tab, { + command: "addNewVaultItemFromOverlay", + addNewCipherType, + }).catch((error) => this.logService.error(error)); } /** @@ -1398,18 +1429,154 @@ export class OverlayBackground implements OverlayBackgroundInterface { { addNewCipherType, login, card, identity }: OverlayAddNewItemMessage, sender: chrome.runtime.MessageSender, ) { - if (!addNewCipherType) { + if ( + !this.currentAddNewItemData || + sender.tab.id !== this.currentAddNewItemData.sender.tab.id || + !addNewCipherType || + this.currentAddNewItemData.addNewCipherType !== addNewCipherType + ) { return; } + if (login && this.isAddingNewLogin()) { + this.updateCurrentAddNewItemLogin(login); + } + + if (card && this.isAddingNewCard()) { + this.updateCurrentAddNewItemCard(card); + } + + if (identity && this.isAddingNewIdentity()) { + this.updateCurrentAddNewItemIdentity(identity); + } + + this.addNewVaultItemSubject.next(this.currentAddNewItemData); + } + + /** + * Identifies if the current add new item data is for adding a new login. + */ + private isAddingNewLogin() { + return this.currentAddNewItemData.addNewCipherType === CipherType.Login; + } + + /** + * Identifies if the current add new item data is for adding a new card. + */ + private isAddingNewCard() { + return this.currentAddNewItemData.addNewCipherType === CipherType.Card; + } + + /** + * Identifies if the current add new item data is for adding a new identity. + */ + private isAddingNewIdentity() { + return this.currentAddNewItemData.addNewCipherType === CipherType.Identity; + } + + /** + * Updates the current add new item data with the provided login data. If the + * login data is already present, the data will be merged with the existing data. + * + * @param login - The login data captured from the extension message + */ + private updateCurrentAddNewItemLogin(login: NewLoginCipherData) { + if (!this.currentAddNewItemData.login) { + this.currentAddNewItemData.login = login; + return; + } + + const currentLoginData = this.currentAddNewItemData.login; + this.currentAddNewItemData.login = { + uri: login.uri || currentLoginData.uri, + hostname: login.hostname || currentLoginData.hostname, + username: login.username || currentLoginData.username, + password: login.password || currentLoginData.password, + }; + } + + /** + * Updates the current add new item data with the provided card data. If the + * card data is already present, the data will be merged with the existing data. + * + * @param card - The card data captured from the extension message + */ + private updateCurrentAddNewItemCard(card: NewCardCipherData) { + if (!this.currentAddNewItemData.card) { + this.currentAddNewItemData.card = card; + return; + } + + const currentCardData = this.currentAddNewItemData.card; + this.currentAddNewItemData.card = { + cardholderName: card.cardholderName || currentCardData.cardholderName, + number: card.number || currentCardData.number, + expirationMonth: card.expirationMonth || currentCardData.expirationMonth, + expirationYear: card.expirationYear || currentCardData.expirationYear, + expirationDate: card.expirationDate || currentCardData.expirationDate, + cvv: card.cvv || currentCardData.cvv, + }; + } + + /** + * Updates the current add new item data with the provided identity data. If the + * identity data is already present, the data will be merged with the existing data. + * + * @param identity - The identity data captured from the extension message + */ + private updateCurrentAddNewItemIdentity(identity: NewIdentityCipherData) { + if (!this.currentAddNewItemData.identity) { + this.currentAddNewItemData.identity = identity; + return; + } + + const currentIdentityData = this.currentAddNewItemData.identity; + this.currentAddNewItemData.identity = { + title: identity.title || currentIdentityData.title, + firstName: identity.firstName || currentIdentityData.firstName, + middleName: identity.middleName || currentIdentityData.middleName, + lastName: identity.lastName || currentIdentityData.lastName, + fullName: identity.fullName || currentIdentityData.fullName, + address1: identity.address1 || currentIdentityData.address1, + address2: identity.address2 || currentIdentityData.address2, + address3: identity.address3 || currentIdentityData.address3, + city: identity.city || currentIdentityData.city, + state: identity.state || currentIdentityData.state, + postalCode: identity.postalCode || currentIdentityData.postalCode, + country: identity.country || currentIdentityData.country, + company: identity.company || currentIdentityData.company, + phone: identity.phone || currentIdentityData.phone, + email: identity.email || currentIdentityData.email, + username: identity.username || currentIdentityData.username, + }; + } + + /** + * Handles building a new cipher and opening the add/edit vault item popout. + * + * @param login - The login data captured from the extension message + * @param card - The card data captured from the extension message + * @param identity - The identity data captured from the extension message + * @param sender - The sender of the extension message + */ + private async buildCipherAndOpenAddEditVaultItemPopout({ + login, + card, + identity, + sender, + }: CurrentAddNewItemData) { const cipherView: CipherView = this.buildNewVaultItemCipherView({ - addNewCipherType, login, card, identity, }); - if (cipherView) { + if (!cipherView) { + this.currentAddNewItemData = null; + return; + } + + try { this.closeInlineMenu(sender); await this.cipherService.setAddEditCipherInfo({ cipher: cipherView, @@ -1418,32 +1585,30 @@ export class OverlayBackground implements OverlayBackgroundInterface { await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); + } catch (error) { + this.logService.error("Error building cipher and opening add/edit vault item popout", error); } + + this.currentAddNewItemData = null; } /** * Builds and returns a new cipher view with the provided vault item data. * - * @param addNewCipherType - The type of cipher to add * @param login - The login data captured from the extension message * @param card - The card data captured from the extension message * @param identity - The identity data captured from the extension message */ - private buildNewVaultItemCipherView({ - addNewCipherType, - login, - card, - identity, - }: OverlayAddNewItemMessage) { - if (login && addNewCipherType === CipherType.Login) { + private buildNewVaultItemCipherView({ login, card, identity }: OverlayAddNewItemMessage) { + if (login && this.isAddingNewLogin()) { return this.buildLoginCipherView(login); } - if (card && addNewCipherType === CipherType.Card) { + if (card && this.isAddingNewCard()) { return this.buildCardCipherView(card); } - if (identity && addNewCipherType === CipherType.Identity) { + if (identity && this.isAddingNewIdentity()) { return this.buildIdentityCipherView(identity); } } @@ -1708,7 +1873,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.resetFocusedFieldSubFrameOffsets(sender); this.cancelInlineMenuFadeInAndPositionUpdate(); - void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender); + this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender).catch((error) => + this.logService.error(error), + ); this.repositionInlineMenuSubject.next(sender); } @@ -1898,14 +2065,14 @@ export class OverlayBackground implements OverlayBackgroundInterface { filledByCipherType: this.focusedFieldData?.filledByCipherType, showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), }); - void this.updateInlineMenuPosition( + this.updateInlineMenuPosition( { overlayElement: isInlineMenuListPort ? AutofillOverlayElement.List : AutofillOverlayElement.Button, }, port.sender, - ); + ).catch((error) => this.logService.error(error)); }; /** diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 15eeff75cd9..1d5ec605320 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -1099,7 +1099,9 @@ describe("AutofillOverlayContentService", () => { selectFieldElement.dispatchEvent(new Event("focus")); await flushPromises(); - expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu"); + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); }); it("updates the most recently focused field", async () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 9a45403edcb..064c76b657e 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -249,10 +249,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * to the background script to add a new cipher. */ async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) { - if (!(await this.isInlineMenuListVisible())) { - return; - } - const command = "autofillOverlayAddNewVaultItem"; if (addNewCipherType === CipherType.Login) { @@ -680,7 +676,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if (elementIsSelectElement(formFieldElement)) { - await this.sendExtensionMessage("closeAutofillInlineMenu"); + await this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); return; } @@ -763,7 +761,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ private async updateMostRecentlyFocusedField( formFieldElement: ElementWithOpId, ) { - if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) { + if ( + !formFieldElement || + !elementIsFillableFormField(formFieldElement) || + elementIsSelectElement(formFieldElement) + ) { return; } From 8fcf717ec4661130732bd56b67b975bf70f67ab4 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:00:29 -0500 Subject: [PATCH 10/31] [PM-10437][PM-10438] Copy toast message (#10366) * add option to include a label for copy success message * add label text to copy success messages for all cipher types --- .../directives/copy-click.directive.spec.ts | 26 ++++++++++++++++++- .../src/directives/copy-click.directive.ts | 12 ++++++++- .../additional-options.component.html | 1 + .../card-details-view.component.html | 2 ++ .../custom-fields-v2.component.html | 2 ++ .../view-identity-sections.component.html | 9 +++++++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/libs/angular/src/directives/copy-click.directive.spec.ts b/libs/angular/src/directives/copy-click.directive.spec.ts index a64b6658b3a..29466f7fbe3 100644 --- a/libs/angular/src/directives/copy-click.directive.spec.ts +++ b/libs/angular/src/directives/copy-click.directive.spec.ts @@ -12,12 +12,14 @@ import { CopyClickDirective } from "./copy-click.directive"; + `, }) class TestCopyClickComponent { @ViewChild("noToast") noToastButton: ElementRef; @ViewChild("infoToast") infoToastButton: ElementRef; @ViewChild("successToast") successToastButton: ElementRef; + @ViewChild("toastWithLabel") toastWithLabelButton: ElementRef; } describe("CopyClickDirective", () => { @@ -32,7 +34,17 @@ describe("CopyClickDirective", () => { await TestBed.configureTestingModule({ declarations: [CopyClickDirective, TestCopyClickComponent], providers: [ - { provide: I18nService, useValue: { t: (key: string) => key } }, + { + provide: I18nService, + useValue: { + t: (key: string, ...rest: string[]) => { + if (rest?.length) { + return `${key} ${rest.join("")}`; + } + return key; + }, + }, + }, { provide: PlatformUtilsService, useValue: { copyToClipboard } }, { provide: ToastService, useValue: { showToast } }, ], @@ -87,4 +99,16 @@ describe("CopyClickDirective", () => { variant: "info", }); }); + + it('includes label in toast message when "copyLabel" is set', () => { + const toastWithLabelButton = fixture.componentInstance.toastWithLabelButton.nativeElement; + + toastWithLabelButton.click(); + + expect(showToast).toHaveBeenCalledWith({ + message: "valueCopied Content", + title: null, + variant: "success", + }); + }); }); diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/angular/src/directives/copy-click.directive.ts index 0d764c95edb..c0b7fac02aa 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/angular/src/directives/copy-click.directive.ts @@ -20,6 +20,12 @@ export class CopyClickDirective { @Input("appCopyClick") valueToCopy = ""; + /** + * When set, the toast displayed will show ` copied` + * instead of the default messaging. + */ + @Input() valueLabel: string; + /** * When set without a value, a success toast will be shown when the value is copied * @example @@ -47,10 +53,14 @@ export class CopyClickDirective { this.platformUtilsService.copyToClipboard(this.valueToCopy); if (this._showToast) { + const message = this.valueLabel + ? this.i18nService.t("valueCopied", this.valueLabel) + : this.i18nService.t("copySuccessful"); + this.toastService.showToast({ variant: this.toastVariant, title: null, - message: this.i18nService.t("copySuccessful"), + message, }); } } diff --git a/libs/vault/src/cipher-view/additional-options/additional-options.component.html b/libs/vault/src/cipher-view/additional-options/additional-options.component.html index 1f33bb78825..59b2753945a 100644 --- a/libs/vault/src/cipher-view/additional-options/additional-options.component.html +++ b/libs/vault/src/cipher-view/additional-options/additional-options.component.html @@ -13,6 +13,7 @@ type="button" [appCopyClick]="notes" showToast + [valueLabel]="'note' | i18n" [appA11yTitle]="'copyValue' | i18n" > diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.html b/libs/vault/src/cipher-view/card-details/card-details-view.component.html index 108b148016b..588849eaee4 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.html +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.html @@ -37,6 +37,7 @@ type="button" [appCopyClick]="card.number" showToast + [valueLabel]="'number' | i18n" [appA11yTitle]="'copyValue' | i18n" data-testid="copy-number" > @@ -75,6 +76,7 @@ type="button" [appCopyClick]="card.code" showToast + [valueLabel]="'securityCode' | i18n" [appA11yTitle]="'copyValue' | i18n" data-testid="copy-code" > diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index 7ffec66427b..d4c29cf262b 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -17,6 +17,7 @@ type="button" [appCopyClick]="field.value" showToast + [valueLabel]="field.name" [appA11yTitle]="'copyValue' | i18n" > @@ -30,6 +31,7 @@ type="button" [appCopyClick]="field.value" showToast + [valueLabel]="field.name" [appA11yTitle]="'copyValue' | i18n" > 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 index bd6ed2e00bd..d12a729f99a 100644 --- 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 @@ -14,6 +14,7 @@ [appA11yTitle]="'copyName' | i18n" [appCopyClick]="cipher.identity.fullName" showToast + [valueLabel]="'name' | i18n" > @@ -26,6 +27,7 @@ [appA11yTitle]="'copyUsername' | i18n" [appCopyClick]="cipher.identity.username" showToast + [valueLabel]="'username' | i18n" > @@ -38,6 +40,7 @@ [appA11yTitle]="'copyCompany' | i18n" [appCopyClick]="cipher.identity.company" showToast + [valueLabel]="'company' | i18n" > @@ -66,6 +69,7 @@ [appA11yTitle]="'copySSN' | i18n" [appCopyClick]="cipher.identity.ssn" showToast + [valueLabel]="'ssn' | i18n" > @@ -91,6 +95,7 @@ [appA11yTitle]="'copyPassportNumber' | i18n" [appCopyClick]="cipher.identity.passportNumber" showToast + [valueLabel]="'passportNumber' | i18n" > @@ -103,6 +108,7 @@ [appA11yTitle]="'copyLicenseNumber' | i18n" [appCopyClick]="cipher.identity.licenseNumber" showToast + [valueLabel]="'licenseNumber' | i18n" > @@ -124,6 +130,7 @@ [appA11yTitle]="'copyEmail' | i18n" [appCopyClick]="cipher.identity.email" showToast + [valueLabel]="'email' | i18n" > @@ -136,6 +143,7 @@ [appA11yTitle]="'copyPhone' | i18n" [appCopyClick]="cipher.identity.phone" showToast + [valueLabel]="'phone' | i18n" > @@ -155,6 +163,7 @@ [appA11yTitle]="'copyAddress' | i18n" [appCopyClick]="addressFields" showToast + [valueLabel]="'address' | i18n" > From 334393ff16d5ce1f8fc144394f12992c5400860a Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:06:55 -0500 Subject: [PATCH 11/31] decrease headings within browser extension to h6 styling (#10383) --- .../additional-options-section.component.html | 2 +- .../card-details-section.component.html | 2 +- .../components/custom-fields/custom-fields.component.html | 2 +- .../components/identity/identity.component.html | 8 ++++---- .../item-details/item-details-section.component.html | 2 +- .../login-details-section.component.html | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html index 3b14ee37d21..0b0c729de89 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html @@ -1,6 +1,6 @@ -

{{ "additionalOptions" | i18n }}

+

{{ "additionalOptions" | i18n }}

diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html index 9464ad96762..161f193108a 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html @@ -1,6 +1,6 @@ -

+

{{ getSectionHeading() }}

diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 368ba649cbb..1be4d922c53 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -1,6 +1,6 @@ -

{{ "customFields" | i18n }}

+

{{ "customFields" | i18n }}

-

{{ "personalDetails" | i18n }}

+

{{ "personalDetails" | i18n }}

@@ -51,7 +51,7 @@
-

{{ "identification" | i18n }}

+

{{ "identification" | i18n }}

@@ -90,7 +90,7 @@
-

{{ "contactInfo" | i18n }}

+

{{ "contactInfo" | i18n }}

@@ -109,7 +109,7 @@
-

{{ "address" | i18n }}

+

{{ "address" | i18n }}

diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index 0d79d4c2506..435ba7b4eb8 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -1,6 +1,6 @@ -

{{ "itemDetails" | i18n }}

+

{{ "itemDetails" | i18n }}

+ `, +}) +export class DynamicContentExampleComponent { + initialData = true; + + constructor(private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService) {} + + toggleData() { + if (this.initialData) { + this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData(changedData); + } else { + this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData(initialData); + } + + this.initialData = !this.initialData; + } +} + +export const DynamicContentExample: Story = { + render: (args) => ({ + props: args, + template: "", + }), + decorators: decorators({ + components: [DynamicContentExampleComponent], + routes: [ + { + path: "**", + redirectTo: "dynamic-content-example", + pathMatch: "full", + }, + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "dynamic-content-example", + data: initialData, + children: [ + { + path: "", + component: DynamicContentExampleComponent, + }, + ], + }, + ], + }, + ], + }), +}; diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts new file mode 100644 index 00000000000..569edaae978 --- /dev/null +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts @@ -0,0 +1,35 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ExtensionBitwardenLogoPrimary = svgIcon` + + + +`; + +export const ExtensionBitwardenLogoWhite = svgIcon` + + + +`; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 2c7129db293..2fba41d17ad 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -15,6 +15,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; +import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -82,6 +83,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; @@ -521,6 +523,11 @@ const safeProviders: SafeProvider[] = [ useFactory: getBgService("taskSchedulerService"), deps: [], }), + safeProvider({ + provide: AnonLayoutWrapperDataService, + useClass: ExtensionAnonLayoutWrapperDataService, + deps: [], + }), ]; @NgModule({ diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index 7559e35cdc3..1c082323b1d 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -106,7 +106,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = data.pageIcon; } - if (data.showReadonlyHostname) { + if (data.showReadonlyHostname != null) { this.showReadonlyHostname = data.showReadonlyHostname; } } diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 3af22a704fd..bd3de51c461 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,5 +1,6 @@
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index 966cbd3c0e1..19dafa732ab 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -26,6 +26,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges { @Input() showReadonlyHostname: boolean; @Input() hideLogo: boolean = false; @Input() hideFooter: boolean = false; + @Input() decreaseTopPadding: boolean = false; /** * Max width of the layout content * diff --git a/libs/auth/src/angular/anon-layout/default-anon-layout-wrapper-data.service.ts b/libs/auth/src/angular/anon-layout/default-anon-layout-wrapper-data.service.ts index 43637d481f7..1f09d209445 100644 --- a/libs/auth/src/angular/anon-layout/default-anon-layout-wrapper-data.service.ts +++ b/libs/auth/src/angular/anon-layout/default-anon-layout-wrapper-data.service.ts @@ -4,7 +4,7 @@ import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service import { AnonLayoutWrapperData } from "./anon-layout-wrapper.component"; export class DefaultAnonLayoutWrapperDataService implements AnonLayoutWrapperDataService { - private anonLayoutWrapperDataSubject = new Subject(); + protected anonLayoutWrapperDataSubject = new Subject(); setAnonLayoutWrapperData(data: AnonLayoutWrapperData): void { this.anonLayoutWrapperDataSubject.next(data); diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index 7ddcb1b64b9..711a817bba7 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -52,3 +52,10 @@ $card-icons-base: "../../src/billing/images/cards/"; .sbdocs-preview pre.prismjs { color: white; } + +.sb-show-main app-current-account button { + border: none; + background-color: transparent; + padding-inline: 0px; + padding-block: 0px; +} From 5b47ca1011f67973fd00d7b170103b6936091bce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:13:38 +0000 Subject: [PATCH 27/31] [deps] SM: Update husky to v9.1.4 (#10427) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81ae338ca39..1c79e1ed361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,7 +153,7 @@ "html-loader": "5.0.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.0", - "husky": "9.0.11", + "husky": "9.1.4", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", @@ -23552,12 +23552,13 @@ } }, "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz", + "integrity": "sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==", "dev": true, + "license": "MIT", "bin": { - "husky": "bin.mjs" + "husky": "bin.js" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 3c30f3ec0ad..e4ef970afa1 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "html-loader": "5.0.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.0", - "husky": "9.0.11", + "husky": "9.1.4", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", From af14c3fe6d9c1e0299bda6c956106b0cec32265d Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 7 Aug 2024 05:34:03 -0700 Subject: [PATCH 28/31] [PM-9854] - Send Search Component (#10278) * send list items container * update send list items container * finalize send list container * remove unecessary file * undo change to config * prefer use of takeUntilDestroyed * add send items service * and send list filters and service * undo changes to jest config * add specs for send list filters * Revert "Merge branch 'PM-9853' into PM-9852" This reverts commit 9f65ded13f1dfd38c80da6e0ce4f1d0c48eb8b59, reversing changes made to 63f95600e847afa28ef0e200543c3dd90d5ac233. * add send items service * Revert "Revert "Merge branch 'PM-9853' into PM-9852"" This reverts commit 81e9860c254904700c28985b07b40b36923ca312. * finish send search * fix formControlName * add specs * finalize send search * layout and copy fixes * cleanup * Remove unneeded empty file * Remove the erroneous addition of send-list-filters to vault-export tsconfig * update tests * hide send list filters for non-premium users * fix and add specss * Fix small typo * Re-add missing tests * Remove unused NgZone * Rename selector for send-search --------- Co-authored-by: Daniel James Smith Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 6 + .../popup/send-v2/send-v2.component.html | 24 +++- .../popup/send-v2/send-v2.component.spec.ts | 75 ++++++++---- .../tools/popup/send-v2/send-v2.component.ts | 66 +++++++--- apps/desktop/src/locales/en/messages.json | 6 + apps/web/src/locales/en/messages.json | 6 + libs/tools/send/send-ui/src/index.ts | 3 + .../send-list-filters.component.html | 6 +- .../send-list-filters.component.spec.ts | 66 ++++++++++ .../send-list-filters.component.ts | 10 +- .../send-list-items-container.component.html | 2 +- .../send-list-items-container.component.ts | 3 + .../send-search/send-search.component.html | 8 ++ .../src/send-search/send-search.component.ts | 52 ++++++++ .../src/services/send-items.service.spec.ts | 114 ++++++++++++++++++ .../src/services/send-items.service.ts | 102 ++++++++++++++++ .../send-list-filters.service.spec.ts | 6 +- .../src/services/send-list-filters.service.ts | 12 +- 18 files changed, 514 insertions(+), 53 deletions(-) create mode 100644 libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.spec.ts create mode 100644 libs/tools/send/send-ui/src/send-search/send-search.component.html create mode 100644 libs/tools/send/send-ui/src/send-search/send-search.component.ts create mode 100644 libs/tools/send/send-ui/src/services/send-items.service.spec.ts create mode 100644 libs/tools/send/send-ui/src/services/send-items.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 03009cdfce4..db1f960b9b3 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4097,6 +4097,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index 52f7c3ed8ff..a8dd3e24f29 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -8,12 +8,32 @@ -
+
{{ "sendsNoItemsTitle" | i18n }} {{ "sendsNoItemsMessage" | i18n }}
- + + +
+ + {{ "noItemsMatchSearch" | i18n }} + {{ "clearFiltersOrTryAnother" | i18n }} + +
+ +
+ +
+ + +
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index d7a302b7903..53a0441eecd 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -1,11 +1,12 @@ import { CommonModule } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { RouterLink } from "@angular/router"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { RouterTestingModule } from "@angular/router/testing"; -import { mock } from "jest-mock-extended"; -import { Observable, of } from "rxjs"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of, BehaviorSubject } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; @@ -15,6 +16,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -22,7 +24,10 @@ import { ButtonModule, NoItemsModule } from "@bitwarden/components"; import { NewSendDropdownComponent, SendListItemsContainerComponent, + SendItemsService, + SendSearchComponent, SendListFiltersComponent, + SendListFiltersService, } from "@bitwarden/send-ui"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; @@ -30,31 +35,49 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { SendV2Component } from "./send-v2.component"; +import { SendV2Component, SendState } from "./send-v2.component"; describe("SendV2Component", () => { let component: SendV2Component; let fixture: ComponentFixture; - let sendViews$: Observable; + let sendItemsService: MockProxy; + let sendListFiltersService: SendListFiltersService; + let sendListFiltersServiceFilters$: BehaviorSubject<{ sendType: SendType | null }>; + let sendItemsServiceEmptyList$: BehaviorSubject; + let sendItemsServiceNoFilteredResults$: BehaviorSubject; beforeEach(async () => { - sendViews$ = of([ - { id: "1", name: "Send A" }, - { id: "2", name: "Send B" }, - ] as SendView[]); + sendListFiltersServiceFilters$ = new BehaviorSubject({ sendType: null }); + sendItemsServiceEmptyList$ = new BehaviorSubject(false); + sendItemsServiceNoFilteredResults$ = new BehaviorSubject(false); + + sendItemsService = mock({ + filteredAndSortedSends$: of([ + { id: "1", name: "Send A" }, + { id: "2", name: "Send B" }, + ] as SendView[]), + latestSearchText$: of(""), + }); + + sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); + + sendListFiltersService.filters$ = sendListFiltersServiceFilters$; + sendItemsService.emptyList$ = sendItemsServiceEmptyList$; + sendItemsService.noFilteredResults$ = sendItemsServiceNoFilteredResults$; await TestBed.configureTestingModule({ imports: [ CommonModule, RouterTestingModule, JslibModule, - NoItemsModule, + ReactiveFormsModule, ButtonModule, NoItemsModule, - RouterLink, NewSendDropdownComponent, SendListItemsContainerComponent, SendListFiltersComponent, + SendSearchComponent, + SendV2Component, PopupPageComponent, PopupHeaderComponent, PopOutComponent, @@ -66,21 +89,24 @@ describe("SendV2Component", () => { { provide: AvatarService, useValue: mock() }, { provide: BillingAccountProfileStateService, - useValue: mock(), + useValue: { hasPremiumFromAnySource$: of(false) }, }, { provide: ConfigService, useValue: mock() }, { provide: EnvironmentService, useValue: mock() }, { provide: LogService, useValue: mock() }, { provide: PlatformUtilsService, useValue: mock() }, { provide: SendApiService, useValue: mock() }, - { provide: SendService, useValue: { sendViews$ } }, + { provide: SendItemsService, useValue: mock() }, + { provide: SearchService, useValue: mock() }, + { provide: SendService, useValue: { sendViews$: new BehaviorSubject([]) } }, + { provide: SendItemsService, useValue: sendItemsService }, { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: SendListFiltersService, useValue: sendListFiltersService }, ], }).compileComponents(); fixture = TestBed.createComponent(SendV2Component); component = fixture.componentInstance; - fixture.detectChanges(); }); @@ -88,14 +114,21 @@ describe("SendV2Component", () => { expect(component).toBeTruthy(); }); - it("should sort sends by name on initialization", async () => { - const sortedSends = [ - { id: "1", name: "Send A" }, - { id: "2", name: "Send B" }, - ] as SendView[]; + it("should update the title based on the current filter", () => { + sendListFiltersServiceFilters$.next({ sendType: SendType.File }); + fixture.detectChanges(); + expect(component["title"]).toBe("fileSends"); + }); - await component.ngOnInit(); + it("should set listState to Empty when emptyList$ emits true", () => { + sendItemsServiceEmptyList$.next(true); + fixture.detectChanges(); + expect(component["listState"]).toBe(SendState.Empty); + }); - expect(component.sends).toEqual(sortedSends); + it("should set listState to NoResults when noFilteredResults$ emits true", () => { + sendItemsServiceNoFilteredResults$.next(true); + fixture.detectChanges(); + expect(component["listState"]).toBe(SendState.NoResults); }); }); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index 6ee5f832bea..5c1ec89fde9 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -1,18 +1,20 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; -import { mergeMap, Subject, takeUntil } from "rxjs"; +import { combineLatest } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { ButtonModule, NoItemsModule } from "@bitwarden/components"; +import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; import { NoSendsIcon, NewSendDropdownComponent, SendListItemsContainerComponent, + SendItemsService, + SendSearchComponent, SendListFiltersComponent, + SendListFiltersService, } from "@bitwarden/send-ui"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; @@ -20,6 +22,11 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +export enum SendState { + Empty, + NoResults, +} + @Component({ templateUrl: "send-v2.component.html", standalone: true, @@ -36,29 +43,56 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co NewSendDropdownComponent, SendListItemsContainerComponent, SendListFiltersComponent, + SendSearchComponent, ], }) export class SendV2Component implements OnInit, OnDestroy { sendType = SendType; - private destroy$ = new Subject(); + sendState = SendState; - sends: SendView[] = []; + protected listState: SendState | null = null; + + protected sends$ = this.sendItemsService.filteredAndSortedSends$; + + protected title: string = "allSends"; protected noItemIcon = NoSendsIcon; - constructor(protected sendService: SendService) {} + protected noResultsIcon = Icons.NoResults; - async ngOnInit() { - this.sendService.sendViews$ - .pipe( - mergeMap(async (sends) => { - this.sends = sends.sort((a, b) => a.name.localeCompare(b.name)); - }), - takeUntil(this.destroy$), - ) - .subscribe(); + constructor( + protected sendItemsService: SendItemsService, + protected sendListFiltersService: SendListFiltersService, + ) { + combineLatest([ + this.sendItemsService.emptyList$, + this.sendItemsService.noFilteredResults$, + this.sendListFiltersService.filters$, + ]) + .pipe(takeUntilDestroyed()) + .subscribe(([emptyList, noFilteredResults, currentFilter]) => { + if (currentFilter?.sendType !== null) { + this.title = `${this.sendType[currentFilter.sendType].toLowerCase()}Sends`; + } else { + this.title = "allSends"; + } + + if (emptyList) { + this.listState = SendState.Empty; + return; + } + + if (noFilteredResults) { + this.listState = SendState.NoResults; + return; + } + + this.listState = null; + }); } + ngOnInit(): void {} + ngOnDestroy(): void {} } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 5cfc860d606..14bd61a0922 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3043,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 96a50b54050..be48d1b301d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8795,6 +8795,12 @@ "purchasedSeatsRemoved": { "message": "purchased seats removed" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "includesXMembers": { "message": "for $COUNT$ member", "placeholders": { diff --git a/libs/tools/send/send-ui/src/index.ts b/libs/tools/send/send-ui/src/index.ts index 02326ac2226..d208709c36d 100644 --- a/libs/tools/send/send-ui/src/index.ts +++ b/libs/tools/send/send-ui/src/index.ts @@ -2,4 +2,7 @@ export * from "./icons"; export * from "./send-form"; export { NewSendDropdownComponent } from "./new-send-dropdown/new-send-dropdown.component"; export { SendListItemsContainerComponent } from "./send-list-items-container/send-list-items-container.component"; +export { SendItemsService } from "./services/send-items.service"; +export { SendSearchComponent } from "./send-search/send-search.component"; export { SendListFiltersComponent } from "./send-list-filters/send-list-filters.component"; +export { SendListFiltersService } from "./services/send-list-filters.service"; diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html index e74e2f05627..17f1233d70e 100644 --- a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html @@ -1,7 +1,7 @@ -
- +
+ { + let component: SendListFiltersComponent; + let fixture: ComponentFixture; + let sendListFiltersService: SendListFiltersService; + let billingAccountProfileStateService: MockProxy; + + beforeEach(async () => { + sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); + sendListFiltersService.resetFilterForm = jest.fn(); + billingAccountProfileStateService = mock(); + + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + JslibModule, + ChipSelectComponent, + ReactiveFormsModule, + SendListFiltersComponent, + ], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: SendListFiltersService, useValue: sendListFiltersService }, + { + provide: BillingAccountProfileStateService, + useValue: billingAccountProfileStateService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SendListFiltersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize canAccessPremium$ from BillingAccountProfileStateService", () => { + let canAccessPremium: boolean | undefined; + component["canAccessPremium$"].subscribe((value) => (canAccessPremium = value)); + expect(canAccessPremium).toBe(true); + }); + + it("should call resetFilterForm on ngOnDestroy", () => { + component.ngOnDestroy(); + expect(sendListFiltersService.resetFilterForm).toHaveBeenCalled(); + }); +}); diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts index ccdaa293241..b313ced742a 100644 --- a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts @@ -1,8 +1,10 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy } from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; +import { Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ChipSelectComponent } from "@bitwarden/components"; import { SendListFiltersService } from "../services/send-list-filters.service"; @@ -16,8 +18,14 @@ import { SendListFiltersService } from "../services/send-list-filters.service"; export class SendListFiltersComponent implements OnDestroy { protected filterForm = this.sendListFiltersService.filterForm; protected sendTypes = this.sendListFiltersService.sendTypes; + protected canAccessPremium$: Observable; - constructor(private sendListFiltersService: SendListFiltersService) {} + constructor( + private sendListFiltersService: SendListFiltersService, + billingAccountProfileStateService: BillingAccountProfileStateService, + ) { + this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + } ngOnDestroy(): void { this.sendListFiltersService.resetFilterForm(); diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html index 6a4c6a308ed..586e62cb611 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html @@ -1,7 +1,7 @@

- {{ "allSends" | i18n }} + {{ headerText }}

{{ sends.length }}
diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts index ef7232e97a0..9551fe07ee8 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts @@ -48,6 +48,9 @@ export class SendListItemsContainerComponent { @Input() sends: SendView[] = []; + @Input() + headerText: string; + constructor( protected dialogService: DialogService, protected environmentService: EnvironmentService, diff --git a/libs/tools/send/send-ui/src/send-search/send-search.component.html b/libs/tools/send/send-ui/src/send-search/send-search.component.html new file mode 100644 index 00000000000..55674aa83e5 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-search/send-search.component.html @@ -0,0 +1,8 @@ +
+ + +
diff --git a/libs/tools/send/send-ui/src/send-search/send-search.component.ts b/libs/tools/send/send-ui/src/send-search/send-search.component.ts new file mode 100644 index 00000000000..cd947490dd2 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-search/send-search.component.ts @@ -0,0 +1,52 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { Subject, Subscription, debounceTime, filter } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SearchModule } from "@bitwarden/components"; + +import { SendItemsService } from "../services/send-items.service"; + +const SearchTextDebounceInterval = 200; + +@Component({ + imports: [CommonModule, SearchModule, JslibModule, FormsModule], + standalone: true, + selector: "tools-send-search", + templateUrl: "send-search.component.html", +}) +export class SendSearchComponent { + searchText: string; + + private searchText$ = new Subject(); + + constructor(private sendListItemService: SendItemsService) { + this.subscribeToLatestSearchText(); + this.subscribeToApplyFilter(); + } + + onSearchTextChanged() { + this.searchText$.next(this.searchText); + } + + subscribeToLatestSearchText(): Subscription { + return this.sendListItemService.latestSearchText$ + .pipe( + takeUntilDestroyed(), + filter((data) => !!data), + ) + .subscribe((text) => { + this.searchText = text; + }); + } + + subscribeToApplyFilter(): Subscription { + return this.searchText$ + .pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed()) + .subscribe((data) => { + this.sendListItemService.applyFilter(data); + }); + } +} diff --git a/libs/tools/send/send-ui/src/services/send-items.service.spec.ts b/libs/tools/send/send-ui/src/services/send-items.service.spec.ts new file mode 100644 index 00000000000..92a48df40a1 --- /dev/null +++ b/libs/tools/send/send-ui/src/services/send-items.service.spec.ts @@ -0,0 +1,114 @@ +import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, first, Subject } from "rxjs"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; + +import { SendItemsService } from "./send-items.service"; +import { SendListFiltersService } from "./send-list-filters.service"; + +describe("SendItemsService", () => { + let testBed: TestBed; + let service: SendItemsService; + + const sendServiceMock = mock(); + const sendListFiltersServiceMock = mock(); + const searchServiceMock = mock(); + + beforeEach(() => { + sendServiceMock.sendViews$ = new BehaviorSubject([]); + sendListFiltersServiceMock.filters$ = new BehaviorSubject({ + sendType: null, + }); + sendListFiltersServiceMock.filterFunction$ = new BehaviorSubject((sends: SendView[]) => sends); + searchServiceMock.searchSends.mockImplementation((sends) => sends); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: SendService, useValue: sendServiceMock }, + { provide: SendListFiltersService, useValue: sendListFiltersServiceMock }, + { provide: SearchService, useValue: searchServiceMock }, + SendItemsService, + ], + }); + + service = testBed.inject(SendItemsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should update and sort filteredAndSortedSends$ when filterFunction$ changes", (done) => { + const unsortedSends = [ + { id: "2", name: "Send B", type: 2, disabled: false }, + { id: "1", name: "Send A", type: 1, disabled: false }, + ] as SendView[]; + + (sendServiceMock.sendViews$ as BehaviorSubject).next([...unsortedSends]); + + service.filteredAndSortedSends$.subscribe((filteredAndSortedSends) => { + expect(filteredAndSortedSends).toEqual([unsortedSends[1], unsortedSends[0]]); + done(); + }); + }); + + it("should update loading$ when sends are loading", (done) => { + const sendsLoading$ = new Subject(); + (service as any)._sendsLoading$ = sendsLoading$; + service.loading$.subscribe((loading) => { + expect(loading).toBe(true); + done(); + }); + + sendsLoading$.next(); + }); + + it("should update hasFilterApplied$ when a filter is applied", (done) => { + searchServiceMock.isSearchable.mockImplementation(async () => true); + + service.hasFilterApplied$.subscribe((canSearch) => { + expect(canSearch).toBe(true); + done(); + }); + + service.applyFilter("test"); + }); + + it("should return true for emptyList$ when there are no sends", (done) => { + (sendServiceMock.sendViews$ as BehaviorSubject).next([]); + + service.emptyList$.subscribe((empty) => { + expect(empty).toBe(true); + done(); + }); + }); + + it("should return true for noFilteredResults$ when there are no filtered sends", (done) => { + searchServiceMock.searchSends.mockImplementation(() => []); + + service.noFilteredResults$.pipe(first()).subscribe((noResults) => { + expect(noResults).toBe(true); + done(); + }); + + (sendServiceMock.sendViews$ as BehaviorSubject).next([]); + }); + + it("should call searchService.searchSends when applyFilter is called", (done) => { + const searchText = "Hello"; + service.applyFilter(searchText); + const searchServiceSpy = jest.spyOn(searchServiceMock, "searchSends"); + + service.filteredAndSortedSends$.subscribe(() => { + expect(searchServiceSpy).toHaveBeenCalledWith([], searchText); + done(); + }); + }); +}); diff --git a/libs/tools/send/send-ui/src/services/send-items.service.ts b/libs/tools/send/send-ui/src/services/send-items.service.ts new file mode 100644 index 00000000000..107749b1e63 --- /dev/null +++ b/libs/tools/send/send-ui/src/services/send-items.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from "@angular/core"; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + from, + map, + Observable, + shareReplay, + startWith, + Subject, + switchMap, + tap, +} from "rxjs"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; + +import { SendListFiltersService } from "./send-list-filters.service"; + +/** + * Service for managing the various item lists on the new Vault tab in the browser popup. + */ +@Injectable({ + providedIn: "root", +}) +export class SendItemsService { + private _searchText$ = new BehaviorSubject(""); + + /** + * Subject that emits whenever new sends are being processed/filtered. + * @private + */ + private _sendsLoading$ = new Subject(); + + latestSearchText$: Observable = this._searchText$.asObservable(); + private _sendList$: Observable = this.sendService.sendViews$; + + /** + * Observable that emits the list of sends, filtered and sorted based on the current search text and filters. + * The list is sorted alphabetically by send name. + * @readonly + */ + filteredAndSortedSends$: Observable = combineLatest([ + this._sendList$, + this._searchText$, + this.sendListFiltersService.filterFunction$, + ]).pipe( + tap(() => this._sendsLoading$.next()), + map(([sends, searchText, filterFunction]): [SendView[], string] => [ + filterFunction(sends), + searchText, + ]), + map(([sends, searchText]) => this.searchService.searchSends(sends, searchText)), + map((sends) => sends.sort((a, b) => a.name.localeCompare(b.name))), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + /** + * Observable that indicates whether the service is currently loading sends. + */ + loading$: Observable = this._sendsLoading$ + .pipe(map(() => true)) + .pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 })); + + /** + * Observable that indicates whether a filter is currently applied to the sends. + */ + hasFilterApplied$ = combineLatest([this._searchText$, this.sendListFiltersService.filters$]).pipe( + switchMap(([searchText, filters]) => { + return from(this.searchService.isSearchable(searchText)).pipe( + map( + (isSearchable) => + isSearchable || Object.values(filters).some((filter) => filter !== null), + ), + ); + }), + ); + + /** + * Observable that indicates whether the user's vault is empty. + */ + emptyList$: Observable = this._sendList$.pipe(map((sends) => !sends.length)); + + /** + * Observable that indicates whether there are no sends to show with the current filter. + */ + noFilteredResults$: Observable = this.filteredAndSortedSends$.pipe( + map((sends) => !sends.length), + ); + + constructor( + private sendService: SendService, + private sendListFiltersService: SendListFiltersService, + private searchService: SearchService, + ) {} + + applyFilter(newSearchText: string) { + this._searchText$.next(newSearchText); + } +} diff --git a/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts b/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts index 023d0e32145..f41dab18e6e 100644 --- a/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts +++ b/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts @@ -5,7 +5,7 @@ import { BehaviorSubject, first } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { Send } from "@bitwarden/common/tools/send/models/domain/send"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendListFiltersService } from "./send-list-filters.service"; @@ -47,7 +47,7 @@ describe("SendListFiltersService", () => { }); it("filters disabled sends", (done) => { - const sends = [{ disabled: true }, { disabled: false }, { disabled: true }] as Send[]; + const sends = [{ disabled: true }, { disabled: false }, { disabled: true }] as SendView[]; service.filterFunction$.pipe(first()).subscribe((filterFunction) => { expect(filterFunction(sends)).toEqual([sends[1]]); done(); @@ -67,7 +67,7 @@ describe("SendListFiltersService", () => { { type: SendType.File }, { type: SendType.Text }, { type: SendType.File }, - ] as Send[]; + ] as SendView[]; service.filterFunction$.subscribe((filterFunction) => { expect(filterFunction(sends)).toEqual([sends[1]]); done(); diff --git a/libs/tools/send/send-ui/src/services/send-list-filters.service.ts b/libs/tools/send/send-ui/src/services/send-list-filters.service.ts index 0d2763b880d..5b2c29329b6 100644 --- a/libs/tools/send/send-ui/src/services/send-list-filters.service.ts +++ b/libs/tools/send/send-ui/src/services/send-list-filters.service.ts @@ -4,7 +4,7 @@ import { map, Observable, startWith } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { Send } from "@bitwarden/common/tools/send/models/domain/send"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { ChipSelectOption } from "@bitwarden/components"; @@ -38,11 +38,11 @@ export class SendListFiltersService { ) {} /** - * Observable whose value is a function that filters an array of `Send` objects based on the current filters + * Observable whose value is a function that filters an array of `SendView` objects based on the current filters */ - filterFunction$: Observable<(send: Send[]) => Send[]> = this.filters$.pipe( + filterFunction$: Observable<(send: SendView[]) => SendView[]> = this.filters$.pipe( map( - (filters) => (sends: Send[]) => + (filters) => (sends: SendView[]) => sends.filter((send) => { // do not show disabled sends if (send.disabled) { @@ -64,12 +64,12 @@ export class SendListFiltersService { readonly sendTypes: ChipSelectOption[] = [ { value: SendType.File, - label: this.i18nService.t("file"), + label: this.i18nService.t("sendTypeFile"), icon: "bwi-file", }, { value: SendType.Text, - label: this.i18nService.t("text"), + label: this.i18nService.t("sendTypeText"), icon: "bwi-file-text", }, ]; From f997a094f75b5ae26c35bd194205590064fbdc46 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 7 Aug 2024 16:15:31 +0200 Subject: [PATCH 29/31] [PM-10689] Fix rpm (& deb) packages not starting by following symlink (#10429) * Fix rpm packages not starting by following wrapper-script executable symlink * Restore newline * Add missing space --- apps/desktop/resources/memory-dump-wrapper.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/resources/memory-dump-wrapper.sh b/apps/desktop/resources/memory-dump-wrapper.sh index fa71cd73764..b62c050683a 100644 --- a/apps/desktop/resources/memory-dump-wrapper.sh +++ b/apps/desktop/resources/memory-dump-wrapper.sh @@ -3,6 +3,10 @@ # disable core dumps ulimit -c 0 -APP_PATH=$(dirname "$0") +# might be behind symlink +RAW_PATH=$(readlink -f "$0") +APP_PATH=$(dirname $RAW_PATH) + # pass through all args -$APP_PATH/bitwarden-app "$@" \ No newline at end of file +$APP_PATH/bitwarden-app "$@" + From 181c697ff7ac1712161ee4e8ba2edc2865b87f44 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Wed, 7 Aug 2024 10:30:13 -0400 Subject: [PATCH 30/31] [CL-342] make overflow text wrap in simple dialog (#10418) * add overflow story and fix to simple dialog * Update libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts Co-authored-by: Victoria League --------- Co-authored-by: Victoria League --- .../simple-dialog/simple-dialog.component.html | 9 +++++++-- .../simple-dialog/simple-dialog.stories.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html index 261c1107954..efbc4343cd7 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html @@ -9,11 +9,16 @@ -

+

-
+
({ + props: args, + template: ` + + Alert Dialogdialogdialogdialogdialogdialogdialogdialogdialogdialogdialogdialogdialog + Message Contentcontentcontentcontentcontentcontentcontentcontentcontentcontentcontent + + + + + + `, + }), +}; From 3c7ca7e6140353582714ffe53ef4695e373fad22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:01:06 +0000 Subject: [PATCH 31/31] [deps] SM: Update lint-staged to v15.2.8 (#10430) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 159 +++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 116 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c79e1ed361..0362185a1a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -157,7 +157,7 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.2.7", + "lint-staged": "15.2.8", "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", "postcss": "8.4.38", @@ -17453,9 +17453,10 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -18765,6 +18766,19 @@ "node": ">=4" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -27278,22 +27292,22 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.2.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz", - "integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==", + "version": "15.2.8", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.8.tgz", + "integrity": "sha512-PUWFf2zQzsd9EFU+kM1d7UP+AZDbKFKuj+9JNVTBkhUFhbg4MAt6WfyMMwBfM4lYqd4D2Jwac5iuTu9rVj4zCQ==", "dev": true, "license": "MIT", "dependencies": { "chalk": "~5.3.0", "commander": "~12.1.0", - "debug": "~4.3.4", + "debug": "~4.3.6", "execa": "~8.0.1", - "lilconfig": "~3.1.1", - "listr2": "~8.2.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", "micromatch": "~4.0.7", "pidtree": "~0.6.0", "string-argv": "~0.3.2", - "yaml": "~2.4.2" + "yaml": "~2.5.0" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -27473,16 +27487,16 @@ } }, "node_modules/listr2": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.3.tgz", - "integrity": "sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", "dev": true, "license": "MIT", "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", - "log-update": "^6.0.0", + "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" }, @@ -27812,14 +27826,15 @@ } }, "node_modules/log-update": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", - "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-escapes": "^6.2.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^7.0.0", + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" }, @@ -27831,12 +27846,16 @@ } }, "node_modules/log-update/node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -27847,6 +27866,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -27859,6 +27879,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -27867,15 +27888,16 @@ } }, "node_modules/log-update/node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -27885,13 +27907,15 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" }, @@ -27902,27 +27926,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "mimic-function": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -27935,10 +27990,11 @@ } }, "node_modules/log-update/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -27956,6 +28012,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -27971,6 +28028,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -29393,6 +29451,19 @@ "node": ">=8" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -40308,9 +40379,9 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index e4ef970afa1..2af0969680a 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.2.7", + "lint-staged": "15.2.8", "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", "postcss": "8.4.38",