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