diff --git a/apps/browser/src/admin-console/services/browser-policy.service.ts b/apps/browser/src/admin-console/services/browser-policy.service.ts index 7b649a7a403..2022cfec583 100644 --- a/apps/browser/src/admin-console/services/browser-policy.service.ts +++ b/apps/browser/src/admin-console/services/browser-policy.service.ts @@ -1,11 +1,8 @@ -import { BehaviorSubject, filter, map, Observable, switchMap, tap } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { browserSession, sessionSync } from "../../platform/decorators/session-sync-observable"; @@ -16,31 +13,4 @@ export class BrowserPolicyService extends PolicyService { initializeAs: "array", }) protected _policies: BehaviorSubject; - - constructor(stateService: StateService, organizationService: OrganizationService) { - super(stateService, organizationService); - this._policies.pipe(this.handleActivateAutofillPolicy.bind(this)).subscribe(); - } - - /** - * If the ActivateAutofill policy is enabled, save a flag indicating if we need to - * enable Autofill on page load. - */ - private handleActivateAutofillPolicy(policies$: Observable) { - return policies$.pipe( - map((policies) => policies.find((p) => p.type == PolicyType.ActivateAutofill && p.enabled)), - filter((p) => p != null), - switchMap(async (_) => [ - await this.stateService.getActivateAutoFillOnPageLoadFromPolicy(), - await this.stateService.getEnableAutoFillOnPageLoad(), - ]), - tap(([activated, autofillEnabled]) => { - if (activated === undefined) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setActivateAutoFillOnPageLoadFromPolicy(!autofillEnabled); - } - }), - ); - } } diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 868c18800bd..c70eaf402e3 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -2,6 +2,7 @@ import { mock, mockReset } from "jest-mock-extended"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; @@ -47,21 +48,18 @@ describe("OverlayBackground", () => { }); const settingsService = mock(); const stateService = mock(); + const autofillSettingsService = mock(); const i18nService = mock(); const platformUtilsService = mock(); - const initOverlayElementPorts = (options = { initList: true, initButton: true }) => { + const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { const { initList, initButton } = options; if (initButton) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); buttonPortSpy = overlayBackground["overlayButtonPort"]; } if (initList) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); listPortSpy = overlayBackground["overlayListPort"]; } @@ -76,12 +74,16 @@ describe("OverlayBackground", () => { environmentService, settingsService, stateService, + autofillSettingsService, i18nService, platformUtilsService, ); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - overlayBackground.init(); + + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + + void overlayBackground.init(); }); afterEach(() => { @@ -570,8 +572,8 @@ describe("OverlayBackground", () => { }); describe("autofillOverlayElementClosed message handler", () => { - beforeEach(() => { - initOverlayElementPorts(); + beforeEach(async () => { + await initOverlayElementPorts(); }); it("disconnects the button element port", () => { @@ -635,7 +637,7 @@ describe("OverlayBackground", () => { describe("getAutofillOverlayVisibility message handler", () => { beforeEach(() => { jest - .spyOn(overlayBackground["settingsService"], "getAutoFillOverlayVisibility") + .spyOn(overlayBackground as any, "getOverlayVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); }); @@ -643,7 +645,7 @@ describe("OverlayBackground", () => { sendExtensionRuntimeMessage({ command: "getAutofillOverlayVisibility" }); await flushPromises(); - expect(overlayBackground["overlayVisibility"]).toBe( + expect(await overlayBackground["getOverlayVisibility"]()).toBe( AutofillOverlayVisibility.OnFieldFocus, ); }); @@ -663,8 +665,8 @@ describe("OverlayBackground", () => { }); describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(() => { - initOverlayElementPorts(); + beforeEach(async () => { + await initOverlayElementPorts(); }); it("will check if the overlay list is focused if the list port is open", () => { @@ -693,8 +695,8 @@ describe("OverlayBackground", () => { }); describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", () => { - initOverlayElementPorts({ initList: true, initButton: false }); + it("will send a `focusOverlayList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); sendExtensionRuntimeMessage({ command: "focusAutofillOverlayList" }); @@ -703,14 +705,15 @@ describe("OverlayBackground", () => { }); describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); + beforeEach(async () => { + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.List), + ); listPortSpy = overlayBackground["overlayListPort"]; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); + + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.Button), + ); buttonPortSpy = overlayBackground["overlayButtonPort"]; }); @@ -813,8 +816,8 @@ describe("OverlayBackground", () => { }); describe("updateOverlayHidden", () => { - beforeEach(() => { - initOverlayElementPorts(); + beforeEach(async () => { + await initOverlayElementPorts(); }); it("returns early if the display value is not provided", () => { @@ -984,19 +987,17 @@ describe("OverlayBackground", () => { jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); }); - it("skips setting up the overlay port if the port connection is not for an overlay element", () => { + it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { const port = createPortSpyMock("not-an-overlay-element"); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - overlayBackground["handlePortOnConnect"](port); + await overlayBackground["handlePortOnConnect"](port); expect(port.onMessage.addListener).not.toHaveBeenCalled(); expect(port.postMessage).not.toHaveBeenCalled(); }); it("sets up the overlay list port if the port connection is for the overlay list", async () => { - initOverlayElementPorts({ initList: true, initButton: false }); + await initOverlayElementPorts({ initList: true, initButton: false }); await flushPromises(); expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); @@ -1012,7 +1013,7 @@ describe("OverlayBackground", () => { }); it("sets up the overlay button port if the port connection is for the overlay button", async () => { - initOverlayElementPorts({ initList: false, initButton: true }); + await initOverlayElementPorts({ initList: false, initButton: true }); await flushPromises(); expect(overlayBackground["overlayListPort"]).toBeUndefined(); @@ -1029,7 +1030,7 @@ describe("OverlayBackground", () => { it("gets the system theme", async () => { jest.spyOn(overlayBackground["stateService"], "getTheme").mockResolvedValue(ThemeType.System); - initOverlayElementPorts({ initList: true, initButton: false }); + await initOverlayElementPorts({ initList: true, initButton: false }); await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith( @@ -1039,8 +1040,8 @@ describe("OverlayBackground", () => { }); describe("handleOverlayElementPortMessage", () => { - beforeEach(() => { - initOverlayElementPorts(); + beforeEach(async () => { + await initOverlayElementPorts(); overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index e5c955e89fb..2bb008e63b3 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,6 +1,9 @@ +import { firstValueFrom } from "rxjs"; + import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,7 +24,11 @@ import { } from "../../vault/popup/utils/vault-popout-window"; import { SHOW_AUTOFILL_BUTTON } from "../constants"; import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum"; +import { + InlineMenuVisibilitySetting, + AutofillOverlayElement, + AutofillOverlayPort, +} from "../utils/autofill-overlay.enum"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { @@ -41,7 +48,6 @@ class OverlayBackground implements OverlayBackgroundInterface { private readonly openUnlockPopout = openUnlockPopout; private readonly openViewVaultItemPopout = openViewVaultItemPopout; private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayVisibility: number; private overlayLoginCiphers: Map = new Map(); private pageDetailsForTab: Record = {}; private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; @@ -90,6 +96,7 @@ class OverlayBackground implements OverlayBackgroundInterface { private environmentService: EnvironmentService, private settingsService: SettingsService, private stateService: StateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, ) { @@ -455,10 +462,8 @@ class OverlayBackground implements OverlayBackgroundInterface { /** * Gets the overlay's visibility setting from the settings service. */ - private async getOverlayVisibility(): Promise { - this.overlayVisibility = await this.settingsService.getAutoFillOverlayVisibility(); - - return this.overlayVisibility; + private async getOverlayVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); } /** diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index acd9be2a8ea..94e2356821b 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -34,11 +34,17 @@ import { import { AutofillService as AbstractAutoFillService } from "../../services/abstractions/autofill.service"; import AutofillService from "../../services/autofill.service"; +import { + AutofillSettingsServiceInitOptions, + autofillSettingsServiceFactory, +} from "./autofill-settings-service.factory"; + type AutoFillServiceOptions = FactoryOptions; export type AutoFillServiceInitOptions = AutoFillServiceOptions & CipherServiceInitOptions & StateServiceInitOptions & + AutofillSettingsServiceInitOptions & TotpServiceInitOptions & EventCollectionServiceInitOptions & LogServiceInitOptions & @@ -57,6 +63,7 @@ export function autofillServiceFactory( new AutofillService( await cipherServiceFactory(cache, opts), await stateServiceFactory(cache, opts), + await autofillSettingsServiceFactory(cache, opts), await totpServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts), await logServiceFactory(cache, opts), diff --git a/apps/browser/src/autofill/background/service_factories/autofill-settings-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-settings-service.factory.ts new file mode 100644 index 00000000000..ef9fdac968c --- /dev/null +++ b/apps/browser/src/autofill/background/service_factories/autofill-settings-service.factory.ts @@ -0,0 +1,35 @@ +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; + +import { + policyServiceFactory, + PolicyServiceInitOptions, +} from "../../../admin-console/background/service-factories/policy-service.factory"; +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +export type AutofillSettingsServiceInitOptions = FactoryOptions & + StateProviderInitOptions & + PolicyServiceInitOptions; + +export function autofillSettingsServiceFactory( + cache: { autofillSettingsService?: AutofillSettingsService } & CachedServices, + opts: AutofillSettingsServiceInitOptions, +): Promise { + return factory( + cache, + "autofillSettingsService", + opts, + async () => + new AutofillSettingsService( + await stateProviderFactory(cache, opts), + await policyServiceFactory(cache, opts), + ), + ); +} diff --git a/apps/browser/src/autofill/content/autofiller.ts b/apps/browser/src/autofill/content/autofiller.ts index b9dd335c31f..5f43023d8bd 100644 --- a/apps/browser/src/autofill/content/autofiller.ts +++ b/apps/browser/src/autofill/content/autofiller.ts @@ -1,4 +1,4 @@ -import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils"; +import { setupExtensionDisconnectAction } from "../utils"; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", loadAutofiller); @@ -22,19 +22,11 @@ function loadAutofiller() { }; setupExtensionEventListeners(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises triggerUserFillOnLoad(); - async function triggerUserFillOnLoad() { - const activeUserIdKey = "activeUserId"; - const userKeyStorage = await getFromLocalStorage(activeUserIdKey); - const activeUserId = userKeyStorage[activeUserIdKey]; - const activeUserStorage = await getFromLocalStorage(activeUserId); - if (activeUserStorage?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) { - clearDoFillInterval(); - doFillInterval = setInterval(() => doFillIfNeeded(), 500); - } + function triggerUserFillOnLoad() { + clearDoFillInterval(); + doFillInterval = setInterval(() => doFillIfNeeded(), 500); } function doFillIfNeeded(force = false) { diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 8dc6df6ccda..20f44100a04 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -11,7 +12,10 @@ import { DialogService } from "@bitwarden/components"; import { BrowserApi } from "../../../platform/browser/browser-api"; import { enableAccountSwitching } from "../../../platform/flags"; import { AutofillService } from "../../services/abstractions/autofill.service"; -import { AutofillOverlayVisibility } from "../../utils/autofill-overlay.enum"; +import { + AutofillOverlayVisibility, + InlineMenuVisibilitySetting, +} from "../../utils/autofill-overlay.enum"; @Component({ selector: "app-autofill", @@ -20,7 +24,7 @@ import { AutofillOverlayVisibility } from "../../utils/autofill-overlay.enum"; export class AutofillComponent implements OnInit { protected canOverrideBrowserAutofillSetting = false; protected defaultBrowserAutofillDisabled = false; - protected autoFillOverlayVisibility: number; + protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; protected autoFillOverlayVisibilityOptions: any[]; protected disablePasswordManagerLink: string; enableAutoFillOnPageLoad = false; @@ -35,10 +39,10 @@ export class AutofillComponent implements OnInit { private stateService: StateService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private configService: ConfigServiceAbstraction, private settingsService: SettingsService, private autofillService: AutofillService, private dialogService: DialogService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, ) { this.autoFillOverlayVisibilityOptions = [ { @@ -80,12 +84,17 @@ export class AutofillComponent implements OnInit { this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden(); - this.autoFillOverlayVisibility = - (await this.settingsService.getAutoFillOverlayVisibility()) || AutofillOverlayVisibility.Off; + this.autoFillOverlayVisibility = await firstValueFrom( + this.autofillSettingsService.inlineMenuVisibility$, + ); - this.enableAutoFillOnPageLoad = await this.stateService.getEnableAutoFillOnPageLoad(); - this.autoFillOnPageLoadDefault = - (await this.stateService.getAutoFillOnPageLoadDefault()) ?? true; + this.enableAutoFillOnPageLoad = await firstValueFrom( + this.autofillSettingsService.autofillOnPageLoad$, + ); + + this.autoFillOnPageLoadDefault = await firstValueFrom( + this.autofillSettingsService.autofillOnPageLoadDefault$, + ); const defaultUriMatch = await this.stateService.getDefaultUriMatch(); this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; @@ -95,19 +104,20 @@ export class AutofillComponent implements OnInit { } async updateAutoFillOverlayVisibility() { - const previousAutoFillOverlayVisibility = - await this.settingsService.getAutoFillOverlayVisibility(); - await this.settingsService.setAutoFillOverlayVisibility(this.autoFillOverlayVisibility); + const previousAutoFillOverlayVisibility = await firstValueFrom( + this.autofillSettingsService.inlineMenuVisibility$, + ); + await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); await this.handleUpdatingAutofillOverlayContentScripts(previousAutoFillOverlayVisibility); await this.requestPrivacyPermission(); } async updateAutoFillOnPageLoad() { - await this.stateService.setEnableAutoFillOnPageLoad(this.enableAutoFillOnPageLoad); + await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); } async updateAutoFillOnPageLoadDefault() { - await this.stateService.setAutoFillOnPageLoadDefault(this.autoFillOnPageLoadDefault); + await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); } async saveDefaultUriMatch() { diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index a1922446894..d0377aaa7be 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -1,6 +1,7 @@ import { mock, mockReset } from "jest-mock-extended"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { EventType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; @@ -50,6 +51,7 @@ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock(); const stateService = mock(); + const autofillSettingsService = mock(); const totpService = mock(); const eventCollectionService = mock(); const logService = mock(); @@ -60,6 +62,7 @@ describe("AutofillService", () => { autofillService = new AutofillService( cipherService, stateService, + autofillSettingsService, totpService, eventCollectionService, logService, @@ -83,6 +86,10 @@ describe("AutofillService", () => { tab2 = createChromeTabMock({ id: 2, url: "http://some-url.com" }); tab3 = createChromeTabMock({ id: 3, url: "chrome-extension://some-extension-route" }); jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([tab1, tab2]); + jest + .spyOn(autofillService, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); it("queries all browser tabs and injects the autofill scripts into them", async () => { @@ -134,6 +141,7 @@ describe("AutofillService", () => { it("re-injects the autofill scripts in all tabs", () => { autofillService["autofillScriptPortsSet"] = new Set([mock()]); jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs"); + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -155,9 +163,14 @@ describe("AutofillService", () => { tabMock = createChromeTabMock(); sender = { tab: tabMock, frameId: 1 }; jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); + jest + .spyOn(autofillService, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); }); it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => { + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); + await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true); [autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => { @@ -169,10 +182,23 @@ describe("AutofillService", () => { }); }); + it("skips injecting autofiller script when autofill on load setting is disabled", async () => { + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(false); + + await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true); + + expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, { + file: "content/autofiller.js", + frameId: sender.frameId, + ...defaultExecuteScriptOptions, + }); + }); + it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => { jest - .spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility") + .spyOn(autofillService, "getOverlayVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); await autofillService.injectAutofillScripts(sender.tab, sender.frameId); @@ -190,8 +216,9 @@ describe("AutofillService", () => { it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => { jest - .spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility") + .spyOn(autofillService, "getOverlayVisibility") .mockResolvedValue(AutofillOverlayVisibility.Off); + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); await autofillService.injectAutofillScripts(sender.tab, sender.frameId); @@ -208,6 +235,8 @@ describe("AutofillService", () => { }); it("injects the content-message-handler script if not injecting on page load", async () => { + jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); + await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false); expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { @@ -622,12 +651,12 @@ describe("AutofillService", () => { const totpCode = "123456"; autofillOptions.cipher.login.totp = "totp"; jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true); - jest.spyOn(stateService, "getDisableAutoTotpCopy").mockResolvedValue(false); + jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode); const autofillResult = await autofillService.doAutoFill(autofillOptions); - expect(stateService.getDisableAutoTotpCopy).toHaveBeenCalled(); + expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled(); expect(totpService.getCode).toHaveBeenCalledWith(autofillOptions.cipher.login.totp); expect(autofillResult).toBe(totpCode); }); @@ -635,11 +664,11 @@ describe("AutofillService", () => { it("does not return a TOTP value if the user does not have premium features", async () => { autofillOptions.cipher.login.totp = "totp"; jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(false); - jest.spyOn(stateService, "getDisableAutoTotpCopy").mockResolvedValue(false); + jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); const autofillResult = await autofillService.doAutoFill(autofillOptions); - expect(stateService.getDisableAutoTotpCopy).not.toHaveBeenCalled(); + expect(autofillService.getShouldAutoCopyTotp).not.toHaveBeenCalled(); expect(totpService.getCode).not.toHaveBeenCalled(); expect(autofillResult).toBeNull(); }); @@ -655,12 +684,12 @@ describe("AutofillService", () => { it("returns a null value if the login does not contain a TOTP value", async () => { autofillOptions.cipher.login.totp = undefined; - jest.spyOn(stateService, "getDisableAutoTotpCopy"); + jest.spyOn(autofillService, "getShouldAutoCopyTotp"); jest.spyOn(totpService, "getCode"); const autofillResult = await autofillService.doAutoFill(autofillOptions); - expect(stateService.getDisableAutoTotpCopy).not.toHaveBeenCalled(); + expect(autofillService.getShouldAutoCopyTotp).not.toHaveBeenCalled(); expect(totpService.getCode).not.toHaveBeenCalled(); expect(autofillResult).toBeNull(); }); @@ -679,13 +708,13 @@ describe("AutofillService", () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = true; jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true); - jest.spyOn(stateService, "getDisableAutoTotpCopy").mockResolvedValue(true); + jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false); jest.spyOn(totpService, "getCode"); const autofillResult = await autofillService.doAutoFill(autofillOptions); expect(stateService.getCanAccessPremium).toHaveBeenCalled(); - expect(stateService.getDisableAutoTotpCopy).toHaveBeenCalled(); + expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled(); expect(totpService.getCode).not.toHaveBeenCalled(); expect(autofillResult).toBeNull(); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index a19ef95bf47..80d8c4f77fe 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,6 +1,9 @@ +import { firstValueFrom } from "rxjs"; + import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { EventType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -17,7 +20,7 @@ import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; -import { AutofillOverlayVisibility } from "../utils/autofill-overlay.enum"; +import { InlineMenuVisibilitySetting } from "../utils/autofill-overlay.enum"; import { AutoFillOptions, @@ -41,6 +44,7 @@ export default class AutofillService implements AutofillServiceInterface { constructor( private cipherService: CipherService, private stateService: BrowserStateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private totpService: TotpService, private eventCollectionService: EventCollectionService, private logService: LogService, @@ -93,15 +97,15 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise { - const isUsingAutofillOverlay = - (await this.settingsService.getAutoFillOverlayVisibility()) !== AutofillOverlayVisibility.Off; - const mainAutofillScript = isUsingAutofillOverlay + const mainAutofillScript = (await this.getOverlayVisibility()) ? "bootstrap-autofill-overlay.js" : "bootstrap-autofill.js"; const injectedScripts = [mainAutofillScript]; - if (triggeringOnPageLoad) { + const autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + + if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); } else { await BrowserApi.executeScriptInTab(tab.id, { @@ -190,6 +194,27 @@ export default class AutofillService implements AutofillServiceInterface { return formData; } + /** + * Gets the overlay's visibility setting from the autofill settings service. + */ + async getOverlayVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the setting for automatically copying TOTP upon autofill from the autofill settings service. + */ + async getShouldAutoCopyTotp(): Promise { + return await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); + } + + /** + * Gets the autofill on page load setting from the autofill settings service. + */ + async getAutofillOnPageLoad(): Promise { + return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$); + } + /** * Autofill a given tab with a given login item * @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item @@ -275,12 +300,11 @@ export default class AutofillService implements AutofillServiceInterface { return; } - totp = await this.stateService.getDisableAutoTotpCopy().then((disabled) => { - if (!disabled) { - return this.totpService.getCode(options.cipher.login.totp); - } - return null; - }); + const shouldAutoCopyTotp = await this.getShouldAutoCopyTotp(); + + totp = shouldAutoCopyTotp + ? await this.totpService.getCode(options.cipher.login.totp) + : null; }), ); diff --git a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts index ae07c7c5e92..92723f01f91 100644 --- a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts +++ b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts @@ -20,9 +20,13 @@ const AutofillOverlayVisibility = { OnFieldFocus: 2, } as const; +type InlineMenuVisibilitySetting = + (typeof AutofillOverlayVisibility)[keyof typeof AutofillOverlayVisibility]; + export { AutofillOverlayElement, AutofillOverlayPort, RedirectFocusDirection, AutofillOverlayVisibility, + InlineMenuVisibilitySetting, }; diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index a2ce51c8cc1..94e4435e251 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -109,6 +109,7 @@ function setElementStyles( * Get data from local storage based on the keys provided. * * @param keys - String or array of strings of keys to get from local storage + * @deprecated Do not call this, use state-relevant services instead */ async function getFromLocalStorage(keys: string | string[]): Promise> { return new Promise((resolve) => { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3e169e28fb2..ddd1737fa7f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -43,6 +43,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, + AutofillSettingsService, +} from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -228,6 +232,7 @@ export default class MainBackground { searchService: SearchServiceAbstraction; notificationsService: NotificationsServiceAbstraction; stateService: StateServiceAbstraction; + autofillSettingsService: AutofillSettingsServiceAbstraction; systemService: SystemServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction; eventUploadService: EventUploadServiceAbstraction; @@ -447,6 +452,10 @@ export default class MainBackground { this.stateProvider, ); this.policyService = new BrowserPolicyService(this.stateService, this.organizationService); + this.autofillSettingsService = new AutofillSettingsService( + this.stateProvider, + this.policyService, + ); this.policyApiService = new PolicyApiService( this.policyService, this.apiService, @@ -548,6 +557,7 @@ export default class MainBackground { this.i18nService, this.searchService, this.stateService, + this.autofillSettingsService, this.encryptService, this.cipherFileUploadService, this.configService, @@ -661,6 +671,7 @@ export default class MainBackground { this.autofillService = new AutofillService( this.cipherService, this.stateService, + this.autofillSettingsService, this.totpService, this.eventCollectionService, this.logService, @@ -759,13 +770,13 @@ export default class MainBackground { this.i18nService, this.notificationsService, this.stateService, + this.autofillSettingsService, this.systemService, this.environmentService, this.messagingService, this.logService, this.configService, this.fido2Service, - this.settingsService, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.cryptoService, @@ -802,6 +813,7 @@ export default class MainBackground { this.environmentService, this.settingsService, this.stateService, + this.autofillSettingsService, this.i18nService, this.platformUtilsService, ); @@ -1030,6 +1042,7 @@ export default class MainBackground { this.vaultTimeoutSettingsService.clear(userId), this.keyConnectorService.clear(), this.vaultFilterService.clear(), + // We intentionally do not clear the autofillSettingsService ]); //Needs to be checked before state is cleaned diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index da6c8aaefb2..adf9e6ab3d1 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,5 +1,5 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -39,13 +39,13 @@ export default class RuntimeBackground { private i18nService: I18nService, private notificationsService: NotificationsService, private stateService: BrowserStateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private systemService: SystemService, private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, private logService: LogService, private configService: ConfigServiceAbstraction, private fido2Service: Fido2Service, - private settingsService: SettingsService, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -338,7 +338,7 @@ export default class RuntimeBackground { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises BrowserApi.createNewTab("https://bitwarden.com/browser-start/"); - await this.settingsService.setAutoFillOverlayVisibility( + await this.autofillSettingsService.setInlineMenuVisibility( AutofillOverlayVisibility.OnFieldFocus, ); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 676b00bea61..ead8442af33 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -40,6 +40,10 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { LoginService } from "@bitwarden/common/auth/services/login.service"; +import { + AutofillSettingsService, + AutofillSettingsServiceAbstraction, +} from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -556,6 +560,11 @@ function getBgService(service: keyof MainBackground) { useClass: ForegroundDerivedStateProvider, deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], }, + { + provide: AutofillSettingsServiceAbstraction, + useClass: AutofillSettingsService, + deps: [StateProvider, PolicyService], + }, ], }) export class ServicesModule {} diff --git a/apps/browser/src/popup/settings/options.component.ts b/apps/browser/src/popup/settings/options.component.ts index 9d96c002cd2..b78a3e01013 100644 --- a/apps/browser/src/popup/settings/options.component.ts +++ b/apps/browser/src/popup/settings/options.component.ts @@ -3,11 +3,11 @@ import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { UriMatchType } from "@bitwarden/common/vault/enums"; @@ -45,7 +45,7 @@ export class OptionsComponent implements OnInit { constructor( private messagingService: MessagingService, private stateService: StateService, - private totpService: TotpService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, i18nService: I18nService, private themingService: AbstractThemingService, private settingsService: SettingsService, @@ -84,10 +84,13 @@ export class OptionsComponent implements OnInit { } async ngOnInit() { - this.enableAutoFillOnPageLoad = await this.stateService.getEnableAutoFillOnPageLoad(); + this.enableAutoFillOnPageLoad = await firstValueFrom( + this.autofillSettingsService.autofillOnPageLoad$, + ); - this.autoFillOnPageLoadDefault = - (await this.stateService.getAutoFillOnPageLoadDefault()) ?? true; + this.autoFillOnPageLoadDefault = await firstValueFrom( + this.autofillSettingsService.autofillOnPageLoadDefault$, + ); this.enableAddLoginNotification = !(await this.stateService.getDisableAddLoginNotification()); @@ -99,7 +102,7 @@ export class OptionsComponent implements OnInit { this.showCardsCurrentTab = !(await this.stateService.getDontShowCardsCurrentTab()); this.showIdentitiesCurrentTab = !(await this.stateService.getDontShowIdentitiesCurrentTab()); - this.enableAutoTotpCopy = !(await this.stateService.getDisableAutoTotpCopy()); + this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); this.enableFavicon = !this.settingsService.getDisableFavicon(); @@ -135,15 +138,15 @@ export class OptionsComponent implements OnInit { } async updateAutoTotpCopy() { - await this.stateService.setDisableAutoTotpCopy(!this.enableAutoTotpCopy); + await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy); } async updateAutoFillOnPageLoad() { - await this.stateService.setEnableAutoFillOnPageLoad(this.enableAutoFillOnPageLoad); + await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); } async updateAutoFillOnPageLoadDefault() { - await this.stateService.setAutoFillOnPageLoadDefault(this.autoFillOnPageLoadDefault); + await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); } async updateFavicon() { diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 3c539586333..2a0821d0d1a 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -1,6 +1,10 @@ import { CipherService as AbstractCipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { + AutofillSettingsServiceInitOptions, + autofillSettingsServiceFactory, +} from "../../../autofill/background/service_factories/autofill-settings-service.factory"; import { CipherFileUploadServiceInitOptions, cipherFileUploadServiceFactory, @@ -53,6 +57,7 @@ export type CipherServiceInitOptions = CipherServiceFactoryOptions & I18nServiceInitOptions & SearchServiceInitOptions & StateServiceInitOptions & + AutofillSettingsServiceInitOptions & EncryptServiceInitOptions & ConfigServiceInitOptions; @@ -72,6 +77,7 @@ export function cipherServiceFactory( await i18nServiceFactory(cache, opts), await searchServiceFactory(cache, opts), await stateServiceFactory(cache, opts), + await autofillSettingsServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await cipherFileUploadServiceFactory(cache, opts), await configServiceFactory(cache, opts), diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index a8767a4edcc..8d0a5698521 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -53,6 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent { platformUtilsService: PlatformUtilsService, auditService: AuditService, stateService: StateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, collectionService: CollectionService, messagingService: MessagingService, private route: ActivatedRoute, @@ -160,7 +162,7 @@ export class AddEditComponent extends BaseAddEditComponent { await super.load(); this.showAutoFillOnPageLoadOptions = this.cipher.type === CipherType.Login && - (await this.stateService.getEnableAutoFillOnPageLoad()); + (await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)); } async submit(): Promise { diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 9e326545261..5bf770a2187 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -1,10 +1,11 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { Subject } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { debounceTime, takeUntil } from "rxjs/operators"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -65,6 +66,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private syncService: SyncService, private searchService: SearchService, private stateService: StateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private passwordRepromptService: PasswordRepromptService, private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, @@ -122,9 +124,9 @@ export class CurrentTabComponent implements OnInit, OnDestroy { .subscribe(() => this.searchVault()); // activate autofill on page load if policy is set - if (await this.stateService.getActivateAutoFillOnPageLoadFromPolicy()) { - await this.stateService.setEnableAutoFillOnPageLoad(true); - await this.stateService.setActivateAutoFillOnPageLoadFromPolicy(false); + if (await this.getActivateAutofillOnPageLoadFromPolicy()) { + await this.autofillSettingsService.setAutofillOnPageLoad(true); + await this.autofillSettingsService.setActivateAutofillOnPageLoadFromPolicy(false); this.platformUtilsService.showToast( "info", null, @@ -301,17 +303,25 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.router.navigate(["autofill"]); } + private async getActivateAutofillOnPageLoadFromPolicy(): Promise { + return await firstValueFrom(this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$); + } + async dismissCallout() { - await this.stateService.setDismissedAutofillCallout(true); + await this.autofillSettingsService.setAutofillOnPageLoadCalloutIsDismissed(true); this.showHowToAutofill = false; } private async setCallout() { + const inlineMenuVisibilityIsOff = + (await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$)) === + AutofillOverlayVisibility.Off; + this.showHowToAutofill = this.loginCiphers.length > 0 && - (await this.stateService.getAutoFillOverlayVisibility()) === AutofillOverlayVisibility.Off && - !(await this.stateService.getEnableAutoFillOnPageLoad()) && - !(await this.stateService.getDismissedAutofillCallout()); + inlineMenuVisibilityIsOff && + !(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)) && + !(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadCalloutIsDismissed$)); if (this.showHowToAutofill) { const autofillCommand = await this.platformUtilsService.getAutofillKeyboardShortcut(); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 161b33a0080..51e4d0af990 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -35,6 +35,7 @@ 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 { ClientType } from "@bitwarden/common/enums"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; @@ -176,6 +177,7 @@ export class Main { userVerificationService: UserVerificationService; pinCryptoService: PinCryptoServiceAbstraction; stateService: StateService; + autofillSettingsService: AutofillSettingsServiceAbstraction; organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; @@ -444,6 +446,7 @@ export class Main { this.i18nService, this.searchService, this.stateService, + this.autofillSettingsService, this.encryptService, this.cipherFileUploadService, this.configService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index c621d9ffa9c..db3b817e1e4 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -82,6 +82,10 @@ import { UserVerificationService } from "@bitwarden/common/auth/services/user-ve import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service"; import { WebAuthnLoginPrfCryptoService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-crypto.service"; import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service"; +import { + AutofillSettingsServiceAbstraction, + AutofillSettingsService, +} from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; @@ -334,6 +338,7 @@ import { ModalService } from "./modal.service"; i18nService: I18nServiceAbstraction, searchService: SearchServiceAbstraction, stateService: StateServiceAbstraction, + autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigServiceAbstraction, @@ -345,6 +350,7 @@ import { ModalService } from "./modal.service"; i18nService, searchService, stateService, + autofillSettingsService, encryptService, fileUploadService, configService, @@ -356,6 +362,7 @@ import { ModalService } from "./modal.service"; I18nServiceAbstraction, SearchServiceAbstraction, StateServiceAbstraction, + AutofillSettingsServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, ConfigServiceAbstraction, @@ -908,6 +915,11 @@ import { ModalService } from "./modal.service"; OrganizationApiServiceAbstraction, ], }, + { + provide: AutofillSettingsServiceAbstraction, + useClass: AutofillSettingsService, + deps: [StateProvider, PolicyServiceAbstraction], + }, { provide: BiometricStateService, useClass: DefaultBiometricStateService, diff --git a/libs/common/src/abstractions/settings.service.ts b/libs/common/src/abstractions/settings.service.ts index b5c7f09a4f3..78ed7183c88 100644 --- a/libs/common/src/abstractions/settings.service.ts +++ b/libs/common/src/abstractions/settings.service.ts @@ -10,7 +10,5 @@ export abstract class SettingsService { getEquivalentDomains: (url: string) => Set; setDisableFavicon: (value: boolean) => Promise; getDisableFavicon: () => boolean; - setAutoFillOverlayVisibility: (value: number) => Promise; - getAutoFillOverlayVisibility: () => Promise; clear: (userId?: string) => Promise; } diff --git a/libs/common/src/services/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts similarity index 89% rename from libs/common/src/services/policy.service.spec.ts rename to libs/common/src/admin-console/services/policy/policy.service.spec.ts index ee26ca15e1e..95b159577d3 100644 --- a/libs/common/src/services/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -1,22 +1,22 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; -import { OrganizationService } from "../admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserStatusType, PolicyType } from "../admin-console/enums"; -import { PermissionsApi } from "../admin-console/models/api/permissions.api"; -import { OrganizationData } from "../admin-console/models/data/organization.data"; -import { PolicyData } from "../admin-console/models/data/policy.data"; -import { MasterPasswordPolicyOptions } from "../admin-console/models/domain/master-password-policy-options"; -import { Organization } from "../admin-console/models/domain/organization"; -import { Policy } from "../admin-console/models/domain/policy"; -import { ResetPasswordPolicyOptions } from "../admin-console/models/domain/reset-password-policy-options"; -import { PolicyResponse } from "../admin-console/models/response/policy.response"; -import { PolicyService } from "../admin-console/services/policy/policy.service"; -import { ListResponse } from "../models/response/list.response"; -import { CryptoService } from "../platform/abstractions/crypto.service"; -import { EncryptService } from "../platform/abstractions/encrypt.service"; -import { ContainerService } from "../platform/services/container.service"; -import { StateService } from "../platform/services/state.service"; +import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserStatusType, PolicyType } from "../../../admin-console/enums"; +import { PermissionsApi } from "../../../admin-console/models/api/permissions.api"; +import { OrganizationData } from "../../../admin-console/models/data/organization.data"; +import { PolicyData } from "../../../admin-console/models/data/policy.data"; +import { MasterPasswordPolicyOptions } from "../../../admin-console/models/domain/master-password-policy-options"; +import { Organization } from "../../../admin-console/models/domain/organization"; +import { Policy } from "../../../admin-console/models/domain/policy"; +import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options"; +import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; +import { PolicyService } from "../../../admin-console/services/policy/policy.service"; +import { ListResponse } from "../../../models/response/list.response"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { ContainerService } from "../../../platform/services/container.service"; +import { StateService } from "../../../platform/services/state.service"; describe("PolicyService", () => { let policyService: PolicyService; diff --git a/libs/common/src/autofill/services/autofill-settings.service.ts b/libs/common/src/autofill/services/autofill-settings.service.ts new file mode 100644 index 00000000000..bb3a981b59d --- /dev/null +++ b/libs/common/src/autofill/services/autofill-settings.service.ts @@ -0,0 +1,174 @@ +import { filter, switchMap, tap, firstValueFrom, map, Observable } from "rxjs"; + +import { + AutofillOverlayVisibility, + InlineMenuVisibilitySetting, +} from "../../../../../apps/browser/src/autofill/utils/autofill-overlay.enum"; +import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "../../admin-console/enums/index"; +import { Policy } from "../../admin-console/models/domain/policy"; +import { + AUTOFILL_SETTINGS_DISK, + AUTOFILL_SETTINGS_DISK_LOCAL, + ActiveUserState, + GlobalState, + KeyDefinition, + StateProvider, +} from "../../platform/state"; + +const AUTOFILL_ON_PAGE_LOAD = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "autofillOnPageLoad", { + deserializer: (value: boolean) => value ?? false, +}); + +const AUTOFILL_ON_PAGE_LOAD_DEFAULT = new KeyDefinition( + AUTOFILL_SETTINGS_DISK, + "autofillOnPageLoadDefault", + { + deserializer: (value: boolean) => value ?? false, + }, +); + +const AUTO_COPY_TOTP = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "autoCopyTotp", { + deserializer: (value: boolean) => value ?? false, +}); + +const AUTOFILL_ON_PAGE_LOAD_CALLOUT_DISMISSED = new KeyDefinition( + AUTOFILL_SETTINGS_DISK, + "autofillOnPageLoadCalloutIsDismissed", + { + deserializer: (value: boolean) => value ?? false, + }, +); + +const ACTIVATE_AUTOFILL_ON_PAGE_LOAD_FROM_POLICY = new KeyDefinition( + AUTOFILL_SETTINGS_DISK_LOCAL, + "activateAutofillOnPageLoadFromPolicy", + { + deserializer: (value: boolean) => value ?? false, + }, +); + +const INLINE_MENU_VISIBILITY = new KeyDefinition( + AUTOFILL_SETTINGS_DISK_LOCAL, + "inlineMenuVisibility", + { + deserializer: (value: InlineMenuVisibilitySetting) => value ?? AutofillOverlayVisibility.Off, + }, +); + +export abstract class AutofillSettingsServiceAbstraction { + autofillOnPageLoad$: Observable; + setAutofillOnPageLoad: (newValue: boolean) => Promise; + autofillOnPageLoadDefault$: Observable; + setAutofillOnPageLoadDefault: (newValue: boolean) => Promise; + autoCopyTotp$: Observable; + setAutoCopyTotp: (newValue: boolean) => Promise; + autofillOnPageLoadCalloutIsDismissed$: Observable; + setAutofillOnPageLoadCalloutIsDismissed: (newValue: boolean) => Promise; + activateAutofillOnPageLoadFromPolicy$: Observable; + setActivateAutofillOnPageLoadFromPolicy: (newValue: boolean) => Promise; + inlineMenuVisibility$: Observable; + setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise; + handleActivateAutofillPolicy: (policies: Observable) => Observable; +} + +export class AutofillSettingsService implements AutofillSettingsServiceAbstraction { + private autofillOnPageLoadState: ActiveUserState; + readonly autofillOnPageLoad$: Observable; + + private autofillOnPageLoadDefaultState: ActiveUserState; + readonly autofillOnPageLoadDefault$: Observable; + + private autoCopyTotpState: ActiveUserState; + readonly autoCopyTotp$: Observable; + + private autofillOnPageLoadCalloutIsDismissedState: ActiveUserState; + readonly autofillOnPageLoadCalloutIsDismissed$: Observable; + + private activateAutofillOnPageLoadFromPolicyState: ActiveUserState; + readonly activateAutofillOnPageLoadFromPolicy$: Observable; + + private inlineMenuVisibilityState: GlobalState; + readonly inlineMenuVisibility$: Observable; + + constructor( + private stateProvider: StateProvider, + policyService: PolicyService, + ) { + this.autofillOnPageLoadState = this.stateProvider.getActive(AUTOFILL_ON_PAGE_LOAD); + this.autofillOnPageLoad$ = this.autofillOnPageLoadState.state$.pipe(map((x) => x ?? false)); + + this.autofillOnPageLoadDefaultState = this.stateProvider.getActive( + AUTOFILL_ON_PAGE_LOAD_DEFAULT, + ); + this.autofillOnPageLoadDefault$ = this.autofillOnPageLoadDefaultState.state$.pipe( + map((x) => x ?? true), + ); + + this.autoCopyTotpState = this.stateProvider.getActive(AUTO_COPY_TOTP); + this.autoCopyTotp$ = this.autoCopyTotpState.state$.pipe(map((x) => x ?? false)); + + this.autofillOnPageLoadCalloutIsDismissedState = this.stateProvider.getActive( + AUTOFILL_ON_PAGE_LOAD_CALLOUT_DISMISSED, + ); + this.autofillOnPageLoadCalloutIsDismissed$ = + this.autofillOnPageLoadCalloutIsDismissedState.state$.pipe(map((x) => x ?? false)); + + this.activateAutofillOnPageLoadFromPolicyState = this.stateProvider.getActive( + ACTIVATE_AUTOFILL_ON_PAGE_LOAD_FROM_POLICY, + ); + this.activateAutofillOnPageLoadFromPolicy$ = + this.activateAutofillOnPageLoadFromPolicyState.state$.pipe(map((x) => x ?? false)); + + this.inlineMenuVisibilityState = this.stateProvider.getGlobal(INLINE_MENU_VISIBILITY); + this.inlineMenuVisibility$ = this.inlineMenuVisibilityState.state$.pipe( + map((x) => x ?? AutofillOverlayVisibility.Off), + ); + + policyService.policies$.pipe(this.handleActivateAutofillPolicy.bind(this)).subscribe(); + } + + async setAutofillOnPageLoad(newValue: boolean): Promise { + await this.autofillOnPageLoadState.update(() => newValue); + } + + async setAutofillOnPageLoadDefault(newValue: boolean): Promise { + await this.autofillOnPageLoadDefaultState.update(() => newValue); + } + + async setAutoCopyTotp(newValue: boolean): Promise { + await this.autoCopyTotpState.update(() => newValue); + } + + async setAutofillOnPageLoadCalloutIsDismissed(newValue: boolean): Promise { + await this.autofillOnPageLoadCalloutIsDismissedState.update(() => newValue); + } + + async setActivateAutofillOnPageLoadFromPolicy(newValue: boolean): Promise { + await this.activateAutofillOnPageLoadFromPolicyState.update(() => newValue); + } + + async setInlineMenuVisibility(newValue: InlineMenuVisibilitySetting): Promise { + await this.inlineMenuVisibilityState.update(() => newValue); + } + + /** + * If the ActivateAutofill policy is enabled, save a flag indicating if we need to + * enable Autofill on page load. + */ + handleActivateAutofillPolicy(policies$: Observable): Observable { + return policies$.pipe( + map((policies) => policies.find((p) => p.type == PolicyType.ActivateAutofill && p.enabled)), + filter((p) => p != null), + switchMap(async (_) => [ + await firstValueFrom(this.activateAutofillOnPageLoadFromPolicy$), + await firstValueFrom(this.autofillOnPageLoad$), + ]), + tap(([activated, autofillEnabled]) => { + if (activated === undefined) { + void this.setActivateAutofillOnPageLoadFromPolicy(!autofillEnabled); + } + }), + ); + } +} diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index e3148e3523f..b6300239028 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -71,8 +71,6 @@ export abstract class StateService { setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise; getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; - getAutoFillOnPageLoadDefault: (options?: StorageOptions) => Promise; - setAutoFillOnPageLoadDefault: (value: boolean, options?: StorageOptions) => Promise; getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise; setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; @@ -243,8 +241,6 @@ export abstract class StateService { setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise; getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise; setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise; - getDisableAutoTotpCopy: (options?: StorageOptions) => Promise; - setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise; getDisableBadgeCounter: (options?: StorageOptions) => Promise; setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise; getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise; @@ -264,8 +260,6 @@ export abstract class StateService { setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise; getDisableGa: (options?: StorageOptions) => Promise; setDisableGa: (value: boolean, options?: StorageOptions) => Promise; - getDismissedAutofillCallout: (options?: StorageOptions) => Promise; - setDismissedAutofillCallout: (value: boolean, options?: StorageOptions) => Promise; getDontShowCardsCurrentTab: (options?: StorageOptions) => Promise; setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise; getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise; @@ -294,10 +288,6 @@ export abstract class StateService { setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; getEnableAlwaysOnTop: (options?: StorageOptions) => Promise; setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise; - getAutoFillOverlayVisibility: (options?: StorageOptions) => Promise; - setAutoFillOverlayVisibility: (value: number, options?: StorageOptions) => Promise; - getEnableAutoFillOnPageLoad: (options?: StorageOptions) => Promise; - setEnableAutoFillOnPageLoad: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegration: (options?: StorageOptions) => Promise; setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise; @@ -486,13 +476,6 @@ export abstract class StateService { getAvatarColor: (options?: StorageOptions) => Promise; setAvatarColor: (value: string, options?: StorageOptions) => Promise; - getActivateAutoFillOnPageLoadFromPolicy: ( - options?: StorageOptions, - ) => Promise; - setActivateAutoFillOnPageLoadFromPolicy: ( - value: boolean, - options?: StorageOptions, - ) => Promise; getSMOnboardingTasks: ( options?: StorageOptions, ) => Promise>>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 9c463b767c9..d3a7da45dac 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -216,20 +216,16 @@ export class AccountProfile { export class AccountSettings { autoConfirmFingerPrints?: boolean; - autoFillOnPageLoadDefault?: boolean; biometricUnlock?: boolean; clearClipboard?: number; collapsedGroupings?: string[]; defaultUriMatch?: UriMatchType; disableAutoBiometricsPrompt?: boolean; - disableAutoTotpCopy?: boolean; disableBadgeCounter?: boolean; disableGa?: boolean; - dismissedAutoFillOnPageLoadCallout?: boolean; dontShowCardsCurrentTab?: boolean; dontShowIdentitiesCurrentTab?: boolean; enableAlwaysOnTop?: boolean; - enableAutoFillOnPageLoad?: boolean; enableBiometric?: boolean; enableFullWidth?: boolean; equivalentDomains?: any; @@ -246,7 +242,6 @@ export class AccountSettings { serverConfig?: ServerConfigData; approveLoginRequests?: boolean; avatarColor?: string; - activateAutoFillOnPageLoadFromPolicy?: boolean; smOnboardingTasks?: Record>; trustDeviceChoiceForDecryption?: boolean; biometricPromptCancelled?: boolean; diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 952e089205c..d5b00cc5d6c 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -34,6 +34,5 @@ export class GlobalState { disableAddLoginNotification?: boolean; disableChangedPasswordNotification?: boolean; disableContextMenuItem?: boolean; - autoFillOverlayVisibility?: number; deepLinkRedirectUrl?: string; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 866dd8e3fc7..5ad305676fd 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,7 +1,6 @@ import { BehaviorSubject, concatMap } from "rxjs"; import { Jsonify, JsonValue } from "type-fest"; -import { AutofillOverlayVisibility } from "../../../../../apps/browser/src/autofill/utils/autofill-overlay.enum"; import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { PolicyData } from "../../admin-console/models/data/policy.data"; import { ProviderData } from "../../admin-console/models/data/provider.data"; @@ -374,24 +373,6 @@ export class StateService< ); } - async getAutoFillOnPageLoadDefault(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.autoFillOnPageLoadDefault ?? true - ); - } - - async setAutoFillOnPageLoadDefault(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.autoFillOnPageLoadDefault = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getBiometricAwaitingAcceptance(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1154,24 +1135,6 @@ export class StateService< ); } - async getDisableAutoTotpCopy(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableAutoTotpCopy ?? false - ); - } - - async setDisableAutoTotpCopy(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableAutoTotpCopy = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getDisableBadgeCounter(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1268,24 +1231,6 @@ export class StateService< ); } - async getDismissedAutofillCallout(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.dismissedAutoFillOnPageLoadCallout ?? false - ); - } - - async setDismissedAutofillCallout(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.dismissedAutoFillOnPageLoadCallout = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getDontShowCardsCurrentTab(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1525,43 +1470,6 @@ export class StateService< ); } - async getAutoFillOverlayVisibility(options?: StorageOptions): Promise { - const locallyStoredOptions = await this.defaultOnDiskLocalOptions(); - const reconciledOptions = this.reconcileOptions(options, locallyStoredOptions); - const globals = await this.getGlobals(reconciledOptions); - - return globals?.autoFillOverlayVisibility ?? AutofillOverlayVisibility.Off; - } - - async setAutoFillOverlayVisibility(value: number, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.autoFillOverlayVisibility = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getEnableAutoFillOnPageLoad(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.enableAutoFillOnPageLoad ?? false - ); - } - - async setEnableAutoFillOnPageLoad(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.enableAutoFillOnPageLoad = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getEnableBrowserIntegration(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -2612,26 +2520,6 @@ export class StateService< ); } - async getActivateAutoFillOnPageLoadFromPolicy(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.activateAutoFillOnPageLoadFromPolicy; - } - - async setActivateAutoFillOnPageLoadFromPolicy( - value: boolean, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.activateAutoFillOnPageLoadFromPolicy = value; - return await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - async getSMOnboardingTasks( options?: StorageOptions, ): Promise>> { diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 5b98c30bed5..bc51af6ae25 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -49,3 +49,8 @@ export const SYNC_STATE = new StateDefinition("sync", "disk", { web: "memory" }) export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", { web: "disk-local", }); + +export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk"); +export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", { + web: "disk-local", +}); diff --git a/libs/common/src/services/settings.service.ts b/libs/common/src/services/settings.service.ts index cab54d0d856..d20efc80c1e 100644 --- a/libs/common/src/services/settings.service.ts +++ b/libs/common/src/services/settings.service.ts @@ -74,14 +74,6 @@ export class SettingsService implements SettingsServiceAbstraction { return this._disableFavicon.getValue(); } - async setAutoFillOverlayVisibility(value: number): Promise { - return await this.stateService.setAutoFillOverlayVisibility(value); - } - - async getAutoFillOverlayVisibility(): Promise { - return await this.stateService.getAutoFillOverlayVisibility(); - } - async clear(userId?: string): Promise { if (userId == null || userId == (await this.stateService.getUserId())) { this._settings.next({}); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 05c26cb5ee8..8ab0f8881bd 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -13,6 +13,7 @@ import { MoveBiometricClientKeyHalfToStateProviders } from "./migrations/14-move import { FolderMigrator } from "./migrations/15-move-folder-state-to-state-provider"; import { LastSyncMigrator } from "./migrations/16-move-last-sync-to-state-provider"; import { EnablePasskeysMigrator } from "./migrations/17-move-enable-passkeys-to-state-providers"; +import { AutofillSettingsKeyMigrator } from "./migrations/18-move-autofill-settings-to-state-providers"; import { FixPremiumMigrator } from "./migrations/3-fix-premium"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; @@ -23,7 +24,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 2; -export const CURRENT_VERSION = 17; +export const CURRENT_VERSION = 18; export type MinVersion = typeof MIN_VERSION; export async function migrate( @@ -56,7 +57,8 @@ export async function migrate( .with(MoveBiometricClientKeyHalfToStateProviders, 13, 14) .with(FolderMigrator, 14, 15) .with(LastSyncMigrator, 15, 16) - .with(EnablePasskeysMigrator, 16, CURRENT_VERSION) + .with(EnablePasskeysMigrator, 16, 17) + .with(AutofillSettingsKeyMigrator, 17, CURRENT_VERSION) .migrate(migrationHelper); } diff --git a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts new file mode 100644 index 00000000000..6a346ab7a3f --- /dev/null +++ b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts @@ -0,0 +1,223 @@ +import { any, MockProxy } from "jest-mock-extended"; + +import { AutofillOverlayVisibility } from "../../../../../apps/browser/src/autofill/utils/autofill-overlay.enum"; +import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { AutofillSettingsKeyMigrator } from "./18-move-autofill-settings-to-state-providers"; + +function exampleJSON() { + return { + global: { + autoFillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2", "user-3"], + "user-1": { + settings: { + autoFillOnPageLoadDefault: true, + enableAutoFillOnPageLoad: true, + dismissedAutoFillOnPageLoadCallout: true, + disableAutoTotpCopy: false, + activateAutoFillOnPageLoadFromPolicy: true, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + global_autofillSettingsLocal_inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick, + "user_user-1_autofillSettings_autoCopyTotp": true, + "user_user-1_autofillSettings_autofillOnPageLoad": true, + "user_user-1_autofillSettings_autofillOnPageLoadCalloutIsDismissed": true, + "user_user-1_autofillSettings_autofillOnPageLoadDefault": true, + "user_user-1_autofillSettingsLocal_activateAutofillOnPageLoadFromPolicy": true, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2", "user-3"], + "user-1": { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const autofillSettingsStateDefinition: { + stateDefinition: StateDefinitionLike; +} = { + stateDefinition: { + name: "autofillSettings", + }, +}; + +describe("ProviderKeysMigrator", () => { + let helper: MockProxy; + let sut: AutofillSettingsKeyMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 17); + sut = new AutofillSettingsKeyMigrator(17, 18); + }); + + it("should remove autofill settings from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("global", { + otherStuff: "otherStuff1", + }); + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it("should set autofill setting values for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToGlobal).toHaveBeenCalledTimes(1); + expect(helper.setToGlobal).toHaveBeenCalledWith( + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "inlineMenuVisibility", + }, + 1, + ); + + expect(helper.setToUser).toHaveBeenCalledTimes(5); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" }, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" }, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" }, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autoCopyTotp" }, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "activateAutofillOnPageLoadFromPolicy", + }, + true, + ); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 16); + sut = new AutofillSettingsKeyMigrator(17, 18); + }); + + it("should null out new values for each account", async () => { + await sut.rollback(helper); + + expect(helper.setToGlobal).toHaveBeenCalledTimes(1); + expect(helper.setToGlobal).toHaveBeenCalledWith( + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "inlineMenuVisibility", + }, + null, + ); + + expect(helper.setToUser).toHaveBeenCalledTimes(5); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" }, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" }, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" }, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsStateDefinition, key: "autoCopyTotp" }, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "activateAutofillOnPageLoadFromPolicy", + }, + null, + ); + }); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("global", { + autoFillOverlayVisibility: 1, + otherStuff: "otherStuff1", + }); + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + otherStuff: "otherStuff2", + autoFillOnPageLoadDefault: true, + enableAutoFillOnPageLoad: true, + dismissedAutoFillOnPageLoadCallout: true, + disableAutoTotpCopy: false, + activateAutoFillOnPageLoadFromPolicy: true, + }, + otherStuff: "otherStuff3", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("user-3", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts new file mode 100644 index 00000000000..bbd09ae83d0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts @@ -0,0 +1,262 @@ +import { InlineMenuVisibilitySetting } from "../../../../../apps/browser/src/autofill/utils/autofill-overlay.enum"; +import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountState = { + settings?: { + autoFillOnPageLoadDefault?: boolean; + enableAutoFillOnPageLoad?: boolean; + dismissedAutoFillOnPageLoadCallout?: boolean; + disableAutoTotpCopy?: boolean; + activateAutoFillOnPageLoadFromPolicy?: InlineMenuVisibilitySetting; + }; +}; + +type ExpectedGlobalState = { autoFillOverlayVisibility?: InlineMenuVisibilitySetting }; + +const autofillSettingsStateDefinition: { + stateDefinition: StateDefinitionLike; +} = { + stateDefinition: { + name: "autofillSettings", + }, +}; + +export class AutofillSettingsKeyMigrator extends Migrator<17, 18> { + async migrate(helper: MigrationHelper): Promise { + // global state (e.g. "autoFillOverlayVisibility -> inlineMenuVisibility") + const globalState = await helper.get("global"); + + if (globalState?.autoFillOverlayVisibility != null) { + await helper.setToGlobal( + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "inlineMenuVisibility", + }, + globalState.autoFillOverlayVisibility, + ); + + // delete `autoFillOverlayVisibility` from state global + delete globalState.autoFillOverlayVisibility; + + await helper.set("global", globalState); + } + + // account state (e.g. account settings -> state provider framework keys) + const accounts = await helper.getAccounts(); + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + + // migrate account state + async function migrateAccount(userId: string, account: ExpectedAccountState): Promise { + let updateAccount = false; + const accountSettings = account?.settings; + + if (accountSettings?.autoFillOnPageLoadDefault != null) { + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" }, + accountSettings.autoFillOnPageLoadDefault, + ); + delete account.settings.autoFillOnPageLoadDefault; + updateAccount = true; + } + + if (accountSettings?.enableAutoFillOnPageLoad != null) { + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" }, + accountSettings?.enableAutoFillOnPageLoad, + ); + delete account.settings.enableAutoFillOnPageLoad; + updateAccount = true; + } + + if (accountSettings?.dismissedAutoFillOnPageLoadCallout != null) { + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" }, + accountSettings?.dismissedAutoFillOnPageLoadCallout, + ); + delete account.settings.dismissedAutoFillOnPageLoadCallout; + updateAccount = true; + } + + if (accountSettings?.disableAutoTotpCopy != null) { + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autoCopyTotp" }, + // invert the value to match the new naming convention + !accountSettings?.disableAutoTotpCopy, + ); + delete account.settings.disableAutoTotpCopy; + updateAccount = true; + } + + if (accountSettings?.activateAutoFillOnPageLoadFromPolicy != null) { + await helper.setToUser( + userId, + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "activateAutofillOnPageLoadFromPolicy", + }, + accountSettings?.activateAutoFillOnPageLoadFromPolicy, + ); + delete account.settings.activateAutoFillOnPageLoadFromPolicy; + updateAccount = true; + } + + if (updateAccount) { + // update the state account settings with the migrated values deleted + await helper.set(userId, account); + } + } + } + + async rollback(helper: MigrationHelper): Promise { + // global state (e.g. "inlineMenuVisibility -> autoFillOverlayVisibility") + const globalState = (await helper.get("global")) || {}; + const inlineMenuVisibility: InlineMenuVisibilitySetting = await helper.getFromGlobal({ + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "inlineMenuVisibility", + }); + + if (inlineMenuVisibility) { + await helper.set("global", { + ...globalState, + autoFillOverlayVisibility: inlineMenuVisibility, + }); + + // remove the global state provider framework key for `inlineMenuVisibility` + await helper.setToGlobal( + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "inlineMenuVisibility", + }, + null, + ); + } + + // account state (e.g. state provider framework keys -> account settings) + const accounts = await helper.getAccounts(); + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + + // rollback account state + async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise { + let updateAccount = false; + let settings = account?.settings || {}; + + const autoFillOnPageLoadDefault: boolean = await helper.getFromUser(userId, { + ...autofillSettingsStateDefinition, + key: "autofillOnPageLoadDefault", + }); + + const enableAutoFillOnPageLoad: boolean = await helper.getFromUser(userId, { + ...autofillSettingsStateDefinition, + key: "autofillOnPageLoad", + }); + + const dismissedAutoFillOnPageLoadCallout: boolean = await helper.getFromUser(userId, { + ...autofillSettingsStateDefinition, + key: "autofillOnPageLoadCalloutIsDismissed", + }); + + const autoCopyTotp: boolean = await helper.getFromUser(userId, { + ...autofillSettingsStateDefinition, + key: "autoCopyTotp", + }); + + const activateAutoFillOnPageLoadFromPolicy: InlineMenuVisibilitySetting = + await helper.getFromUser(userId, { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "activateAutofillOnPageLoadFromPolicy", + }); + + // update new settings and remove the account state provider framework keys for the rolled back values + if (autoFillOnPageLoadDefault != null) { + settings = { ...settings, autoFillOnPageLoadDefault }; + + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" }, + null, + ); + + updateAccount = true; + } + + if (enableAutoFillOnPageLoad != null) { + settings = { ...settings, enableAutoFillOnPageLoad }; + + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" }, + null, + ); + + updateAccount = true; + } + + if (dismissedAutoFillOnPageLoadCallout != null) { + settings = { ...settings, dismissedAutoFillOnPageLoadCallout }; + + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" }, + null, + ); + + updateAccount = true; + } + + if (autoCopyTotp != null) { + // invert the value to match the new naming convention + settings = { ...settings, disableAutoTotpCopy: !autoCopyTotp }; + + await helper.setToUser( + userId, + { ...autofillSettingsStateDefinition, key: "autoCopyTotp" }, + null, + ); + + updateAccount = true; + } + + if (activateAutoFillOnPageLoadFromPolicy != null) { + settings = { ...settings, activateAutoFillOnPageLoadFromPolicy }; + + await helper.setToUser( + userId, + { + stateDefinition: { + name: "autofillSettingsLocal", + }, + key: "activateAutofillOnPageLoadFromPolicy", + }, + null, + ); + + updateAccount = true; + } + + if (updateAccount) { + // commit updated settings to state + await helper.set(userId, { + ...account, + settings, + }); + } + } + } +} diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 1d018110f73..80fe1d7f3b5 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -5,6 +5,7 @@ import { makeStaticByteArray } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { SettingsService } from "../../abstractions/settings.service"; +import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; @@ -97,6 +98,7 @@ const cipherData: CipherData = { describe("Cipher Service", () => { const cryptoService = mock(); const stateService = mock(); + const autofillSettingsService = mock(); const settingsService = mock(); const apiService = mock(); const cipherFileUploadService = mock(); @@ -121,6 +123,7 @@ describe("Cipher Service", () => { i18nService, searchService, stateService, + autofillSettingsService, encryptService, cipherFileUploadService, configService, @@ -266,6 +269,8 @@ describe("Cipher Service", () => { Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey), ); cryptoService.encrypt.mockImplementation(encryptText); + + jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true); }); describe("login encryption", () => { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 2792b41afff..f5256342aa6 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -4,6 +4,7 @@ import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { SettingsService } from "../../abstractions/settings.service"; +import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; @@ -65,6 +66,7 @@ export class CipherService implements CipherServiceAbstraction { private i18nService: I18nService, private searchService: SearchService, private stateService: StateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, private configService: ConfigServiceAbstraction, @@ -1250,6 +1252,10 @@ export class CipherService implements CipherServiceAbstraction { } } + private async getAutofillOnPageLoadDefault() { + return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadDefault$); + } + private async getCipherForUrl( url: string, lastUsed: boolean, @@ -1265,7 +1271,8 @@ export class CipherService implements CipherServiceAbstraction { } if (autofillOnPageLoad) { - const autofillOnPageLoadDefault = await this.stateService.getAutoFillOnPageLoadDefault(); + const autofillOnPageLoadDefault = await this.getAutofillOnPageLoadDefault(); + ciphers = ciphers.filter( (cipher) => cipher.login.autofillOnPageLoad ||