diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts index 8a3a7e6fa8d..62f9dbec824 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts @@ -29,7 +29,7 @@ describe("AutofillInlineMenuContentService", () => { autofillInit = new AutofillInit( domQueryService, domElementVisibilityService, - null, + undefined, autofillInlineMenuContentService, ); autofillInit.init(); @@ -319,6 +319,8 @@ describe("AutofillInlineMenuContentService", () => { describe("handleContainerElementMutationObserverUpdate", () => { let mockMutationRecord: MockProxy; + let mockBodyMutationRecord: MockProxy; + let mockHTMLMutationRecord: MockProxy; let buttonElement: HTMLElement; let listElement: HTMLElement; let isInlineMenuListVisibleSpy: jest.SpyInstance; @@ -329,6 +331,16 @@ describe("AutofillInlineMenuContentService", () => {
`; mockMutationRecord = mock({ target: globalThis.document.body } as any); + mockHTMLMutationRecord = mock({ + target: globalThis.document.body.parentElement, + attributeName: "style", + type: "attributes", + } as any); + mockBodyMutationRecord = mock({ + target: globalThis.document.body, + attributeName: "style", + type: "attributes", + } as any); buttonElement = document.querySelector(".overlay-button") as HTMLElement; listElement = document.querySelector(".overlay-list") as HTMLElement; autofillInlineMenuContentService["buttonElement"] = buttonElement; @@ -343,6 +355,7 @@ describe("AutofillInlineMenuContentService", () => { "isTriggeringExcessiveMutationObserverIterations", ) .mockReturnValue(false); + jest.spyOn(autofillInlineMenuContentService as any, "closeInlineMenu"); }); it("skips handling the mutation if the overlay elements are not present in the DOM", async () => { @@ -373,6 +386,33 @@ describe("AutofillInlineMenuContentService", () => { expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); + it("closes the inline menu if the page body is not sufficiently opaque", async () => { + document.querySelector("html").style.opacity = "0.9"; + document.body.style.opacity = "0"; + autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); + + expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false); + expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); + }); + + it("closes the inline menu if the page html is not sufficiently opaque", async () => { + document.querySelector("html").style.opacity = "0.3"; + document.body.style.opacity = "0.7"; + autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]); + + expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false); + expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); + }); + + it("does not close the inline menu if the page html and body is sufficiently opaque", async () => { + document.querySelector("html").style.opacity = "0.9"; + document.body.style.opacity = "1"; + autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); + + expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(true); + expect(autofillInlineMenuContentService["closeInlineMenu"]).not.toHaveBeenCalled(); + }); + it("skips re-arranging the DOM elements if the last child of the body is non-existent", async () => { document.body.innerHTML = ""; diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index 9bdaf0f965b..de401bf7e28 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -29,8 +29,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private isFirefoxBrowser = globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; - private buttonElement: HTMLElement; - private listElement: HTMLElement; + private buttonElement?: HTMLElement; + private listElement?: HTMLElement; + private htmlMutationObserver: MutationObserver; + private bodyMutationObserver: MutationObserver; + private pageIsOpaque = true; private inlineMenuElementsMutationObserver: MutationObserver; private containerElementMutationObserver: MutationObserver; private mutationObserverIterations = 0; @@ -49,6 +52,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte }; constructor() { + this.checkPageOpacity(); this.setupMutationObserver(); } @@ -281,6 +285,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * that the inline menu elements are always present at the bottom of the menu container. */ private setupMutationObserver = () => { + this.htmlMutationObserver = new MutationObserver(this.handlePageMutations); + this.bodyMutationObserver = new MutationObserver(this.handlePageMutations); + this.inlineMenuElementsMutationObserver = new MutationObserver( this.handleInlineMenuElementMutationObserverUpdate, ); @@ -295,6 +302,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * elements are not modified by the website. */ private observeCustomElements() { + this.htmlMutationObserver?.observe(document.querySelector("html"), { attributes: true }); + this.bodyMutationObserver?.observe(document.body, { attributes: true }); + if (this.buttonElement) { this.inlineMenuElementsMutationObserver?.observe(this.buttonElement, { attributes: true, @@ -395,11 +405,56 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte }); }; + private checkPageOpacity = () => { + this.pageIsOpaque = this.getPageIsOpaque(); + + if (!this.pageIsOpaque) { + this.closeInlineMenu(); + } + }; + + private handlePageMutations = (mutations: MutationRecord[]) => { + for (const mutation of mutations) { + if (mutation.type === "attributes") { + this.checkPageOpacity(); + } + } + }; + + /** + * Checks the opacity of the page body and body parent, since the inline menu experience + * will inherit the opacity, despite being otherwise encapsulated from styling changes + * of parents below the body. Assumes the target element will be a direct child of the page + * `body` (enforced elsewhere). + */ + private getPageIsOpaque() { + // These are computed style values, so we don't need to worry about non-float values + // for `opacity`, here + const htmlOpacity = globalThis.window.getComputedStyle( + globalThis.document.querySelector("html"), + ).opacity; + const bodyOpacity = globalThis.window.getComputedStyle( + globalThis.document.querySelector("body"), + ).opacity; + + // Any value above this is considered "opaque" for our purposes + const opacityThreshold = 0.6; + + return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold; + } + /** * Processes the mutation of the element that contains the inline menu. Will trigger when an * idle moment in the execution of the main thread is detected. */ private processContainerElementMutation = async (containerElement: HTMLElement) => { + // If the computed opacity of the body and parent is not sufficiently opaque, tear + // down and prevent building the inline menu experience. + this.checkPageOpacity(); + if (!this.pageIsOpaque) { + return; + } + const lastChild = containerElement.lastElementChild; const secondToLastChild = lastChild?.previousElementSibling; const lastChildIsInlineMenuList = lastChild === this.listElement; diff --git a/apps/browser/src/platform/badge/badge-browser-api.ts b/apps/browser/src/platform/badge/badge-browser-api.ts index 097c6109743..e8edcd23da4 100644 --- a/apps/browser/src/platform/badge/badge-browser-api.ts +++ b/apps/browser/src/platform/badge/badge-browser-api.ts @@ -1,4 +1,4 @@ -import { map, Observable } from "rxjs"; +import { concat, defer, filter, map, merge, Observable, shareReplay, switchMap } from "rxjs"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -17,19 +17,48 @@ export interface RawBadgeState { export interface BadgeBrowserApi { activeTab$: Observable; + // activeTabs$: Observable; setState(state: RawBadgeState, tabId?: number): Promise; getTabs(): Promise; + getActiveTabs(): Promise; } export class DefaultBadgeBrowserApi implements BadgeBrowserApi { private badgeAction = BrowserApi.getBrowserAction(); private sidebarAction = BrowserApi.getSidebarAction(self); - activeTab$ = fromChromeEvent(chrome.tabs.onActivated).pipe( - map(([tabActiveInfo]) => tabActiveInfo), + private onTabActivated$ = fromChromeEvent(chrome.tabs.onActivated).pipe( + switchMap(async ([activeInfo]) => activeInfo), + shareReplay({ bufferSize: 1, refCount: true }), ); + activeTab$ = concat( + defer(async () => { + const currentTab = await BrowserApi.getTabFromCurrentWindow(); + if (currentTab == null || currentTab.id === undefined) { + return undefined; + } + + return { tabId: currentTab.id, windowId: currentTab.windowId }; + }), + merge( + this.onTabActivated$, + fromChromeEvent(chrome.tabs.onUpdated).pipe( + filter( + ([_, changeInfo]) => + // Only emit if the url was updated + changeInfo.url != undefined, + ), + map(([tabId, _changeInfo, tab]) => ({ tabId, windowId: tab.windowId })), + ), + ), + ).pipe(shareReplay({ bufferSize: 1, refCount: true })); + + getActiveTabs(): Promise { + return BrowserApi.getActiveTabs(); + } + constructor(private platformUtilsService: PlatformUtilsService) {} async setState(state: RawBadgeState, tabId?: number): Promise { diff --git a/apps/browser/src/platform/badge/badge.service.spec.ts b/apps/browser/src/platform/badge/badge.service.spec.ts index 52be2afa71b..71b94e3a0e4 100644 --- a/apps/browser/src/platform/badge/badge.service.spec.ts +++ b/apps/browser/src/platform/badge/badge.service.spec.ts @@ -37,8 +37,9 @@ describe("BadgeService", () => { describe("given a single tab is open", () => { beforeEach(() => { - badgeApi.tabs = [1]; - badgeApi.setActiveTab(tabId); + badgeApi.tabs = [tabId]; + badgeApi.setActiveTabs([tabId]); + badgeApi.setLastActivatedTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); @@ -187,17 +188,18 @@ describe("BadgeService", () => { }); }); - describe("given multiple tabs are open", () => { + describe("given multiple tabs are open, only one active", () => { const tabId = 1; const tabIds = [1, 2, 3]; beforeEach(() => { badgeApi.tabs = tabIds; - badgeApi.setActiveTab(tabId); + badgeApi.setActiveTabs([tabId]); + badgeApi.setLastActivatedTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); - it("sets state for each tab when no other state has been set", async () => { + it("sets general state for active tab when no other state has been set", async () => { const state: BadgeState = { text: "text", backgroundColor: "color", @@ -213,6 +215,67 @@ describe("BadgeService", () => { 3: undefined, }); }); + + it("only updates the active tab when setting state", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + badgeApi.setState.mockReset(); + + await badgeService.setState("state-1", BadgeStatePriority.Default, state, tabId); + await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2); + await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.setState).toHaveBeenCalledTimes(1); + }); + }); + + describe("given multiple tabs are open and multiple are active", () => { + const activeTabIds = [1, 2]; + const tabIds = [1, 2, 3]; + + beforeEach(() => { + badgeApi.tabs = tabIds; + badgeApi.setActiveTabs(activeTabIds); + badgeApi.setLastActivatedTab(1); + badgeServiceSubscription = badgeService.startListening(); + }); + + it("sets general state for active tabs when no other state has been set", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState("state-name", BadgeStatePriority.Default, state); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + 1: state, + 2: state, + 3: undefined, + }); + }); + + it("only updates the active tabs when setting general state", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + badgeApi.setState.mockReset(); + + await badgeService.setState("state-1", BadgeStatePriority.Default, state, 1); + await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2); + await badgeService.setState("state-3", BadgeStatePriority.Default, state, 3); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.setState).toHaveBeenCalledTimes(2); + }); }); }); @@ -222,7 +285,8 @@ describe("BadgeService", () => { beforeEach(() => { badgeApi.tabs = [tabId]; - badgeApi.setActiveTab(tabId); + badgeApi.setActiveTabs([tabId]); + badgeApi.setLastActivatedTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); @@ -491,13 +555,14 @@ describe("BadgeService", () => { }); }); - describe("given multiple tabs are open", () => { + describe("given multiple tabs are open, only one active", () => { const tabId = 1; const tabIds = [1, 2, 3]; beforeEach(() => { badgeApi.tabs = tabIds; - badgeApi.setActiveTab(tabId); + badgeApi.setActiveTabs([tabId]); + badgeApi.setLastActivatedTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); @@ -528,5 +593,62 @@ describe("BadgeService", () => { }); }); }); + + describe("given multiple tabs are open and multiple are active", () => { + const tabId = 1; + const activeTabIds = [1, 2]; + const tabIds = [1, 2, 3]; + + beforeEach(() => { + badgeApi.tabs = tabIds; + badgeApi.setActiveTabs(activeTabIds); + badgeApi.setLastActivatedTab(tabId); + badgeServiceSubscription = badgeService.startListening(); + }); + + it("sets general state for all active tabs when no other state has been set", async () => { + const generalState: BadgeState = { + text: "general-text", + backgroundColor: "general-color", + icon: BadgeIcon.Unlocked, + }; + + await badgeService.setState("general-state", BadgeStatePriority.Default, generalState); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + [tabIds[0]]: generalState, + [tabIds[1]]: generalState, + [tabIds[2]]: undefined, + }); + }); + + it("sets tab-specific state for provided tab", async () => { + const generalState: BadgeState = { + text: "general-text", + backgroundColor: "general-color", + icon: BadgeIcon.Unlocked, + }; + const specificState: BadgeState = { + text: "tab-text", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState("general-state", BadgeStatePriority.Default, generalState); + await badgeService.setState( + "tab-state", + BadgeStatePriority.Default, + specificState, + tabIds[0], + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + [tabIds[0]]: { ...specificState, backgroundColor: "general-color" }, + [tabIds[1]]: generalState, + [tabIds[2]]: undefined, + }); + }); + }); }); }); diff --git a/apps/browser/src/platform/badge/badge.service.ts b/apps/browser/src/platform/badge/badge.service.ts index b3831530e8d..82836370840 100644 --- a/apps/browser/src/platform/badge/badge.service.ts +++ b/apps/browser/src/platform/badge/badge.service.ts @@ -1,13 +1,4 @@ -import { - combineLatest, - concatMap, - distinctUntilChanged, - filter, - map, - pairwise, - startWith, - Subscription, -} from "rxjs"; +import { concatMap, filter, Subscription, withLatestFrom } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { @@ -17,7 +8,6 @@ import { StateProvider, } from "@bitwarden/common/platform/state"; -import { difference } from "./array-utils"; import { BadgeBrowserApi, RawBadgeState } from "./badge-browser-api"; import { DefaultBadgeState } from "./consts"; import { BadgeStatePriority } from "./priority"; @@ -34,14 +24,14 @@ const BADGE_STATES = new KeyDefinition(BADGE_MEMORY, "badgeStates", { }); export class BadgeService { - private states: GlobalState>; + private serviceState: GlobalState>; constructor( private stateProvider: StateProvider, private badgeApi: BadgeBrowserApi, private logService: LogService, ) { - this.states = this.stateProvider.getGlobal(BADGE_STATES); + this.serviceState = this.stateProvider.getGlobal(BADGE_STATES); } /** @@ -49,44 +39,20 @@ export class BadgeService { * Without this the service will not be able to update the badge state. */ startListening(): Subscription { - return combineLatest({ - states: this.states.state$.pipe( - startWith({}), - distinctUntilChanged(), - map((states) => new Set(states ? Object.values(states) : [])), - pairwise(), - map(([previous, current]) => { - const [removed, added] = difference(previous, current); - return { all: current, removed, added }; - }), - filter(({ removed, added }) => removed.size > 0 || added.size > 0), - ), - activeTab: this.badgeApi.activeTab$.pipe(startWith(undefined)), - }) + // React to tab changes + return this.badgeApi.activeTab$ .pipe( - concatMap(async ({ states, activeTab }) => { - const changed = [...states.removed, ...states.added]; - - // If the active tab wasn't changed, we don't need to update the badge. - if (!changed.some((s) => s.tabId === activeTab?.tabId || s.tabId === undefined)) { - return; - } - - try { - const state = this.calculateState(states.all, activeTab?.tabId); - await this.badgeApi.setState(state, activeTab?.tabId); - } catch (error) { - // This usually happens when the user opens a popout because of how the browser treats it - // as a tab in the same window but then won't let you set the badge state for it. - this.logService.warning("Failed to set badge state", error); - } + withLatestFrom(this.serviceState.state$), + filter(([activeTab]) => activeTab != undefined), + concatMap(async ([activeTab, serviceState]) => { + await this.updateBadge(serviceState, activeTab!.tabId); }), ) .subscribe({ - error: (err: unknown) => { + error: (error: unknown) => { this.logService.error( "Fatal error in badge service observable, badge will fail to update", - err, + error, ); }, }); @@ -108,7 +74,12 @@ export class BadgeService { * @param tabId Limit this badge state to a specific tab. If this is not set, the state will be applied to all tabs. */ async setState(name: string, priority: BadgeStatePriority, state: BadgeState, tabId?: number) { - await this.states.update((s) => ({ ...s, [name]: { priority, state, tabId } })); + const newServiceState = await this.serviceState.update((s) => ({ + ...s, + [name]: { priority, state, tabId }, + })); + + await this.updateBadge(newServiceState, tabId); } /** @@ -120,11 +91,21 @@ export class BadgeService { * @param name The name of the state to clear. */ async clearState(name: string) { - await this.states.update((s) => { + let clearedState: StateSetting | undefined; + + const newServiceState = await this.serviceState.update((s) => { + clearedState = s?.[name]; + const newStates = { ...s }; delete newStates[name]; return newStates; }); + + if (clearedState === undefined) { + return; + } + // const activeTabs = await firstValueFrom(this.badgeApi.activeTabs$); + await this.updateBadge(newServiceState, clearedState.tabId); } private calculateState(states: Set, tabId?: number): RawBadgeState { @@ -159,6 +140,52 @@ export class BadgeService { ...mergedState, }; } + + /** + * Common function deduplicating the logic for updating the badge with the current state. + * This will only update the badge if the active tab is the same as the tabId of the latest change. + * If the active tab is not set, it will not update the badge. + * + * @param activeTab The currently active tab. + * @param serviceState The current state of the badge service. If this is null or undefined, an empty set will be assumed. + * @param tabId Tab id for which the the latest state change applied to. Set this to activeTab.tabId to force an update. + */ + private async updateBadge( + // activeTabs: chrome.tabs.Tab[], + serviceState: Record | null | undefined, + tabId: number | undefined, + ) { + const activeTabs = await this.badgeApi.getActiveTabs(); + if (tabId !== undefined && !activeTabs.some((tab) => tab.id === tabId)) { + return; // No need to update the badge if the state is not for the active tab. + } + + const tabIdsToUpdate = tabId ? [tabId] : activeTabs.map((tab) => tab.id); + + for (const tabId of tabIdsToUpdate) { + if (tabId === undefined) { + continue; // Skip if tab id is undefined. + } + + const newBadgeState = this.calculateState(new Set(Object.values(serviceState ?? {})), tabId); + try { + await this.badgeApi.setState(newBadgeState, tabId); + } catch (error) { + this.logService.error("Failed to set badge state", error); + } + } + + if (tabId === undefined) { + // If no tabId was provided we should also update the general badge state + const newBadgeState = this.calculateState(new Set(Object.values(serviceState ?? {}))); + + try { + await this.badgeApi.setState(newBadgeState, tabId); + } catch (error) { + this.logService.error("Failed to set general badge state", error); + } + } + } } /** diff --git a/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts b/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts index 4f91420b273..6708ae52ea5 100644 --- a/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts +++ b/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts @@ -9,15 +9,33 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi { specificStates: Record = {}; generalState?: RawBadgeState; tabs: number[] = []; + activeTabs: number[] = []; - setActiveTab(tabId: number) { + getActiveTabs(): Promise { + return Promise.resolve( + this.activeTabs.map( + (tabId) => + ({ + id: tabId, + windowId: 1, + active: true, + }) as chrome.tabs.Tab, + ), + ); + } + + setActiveTabs(tabs: number[]) { + this.activeTabs = tabs; + } + + setLastActivatedTab(tabId: number) { this._activeTab$.next({ tabId, windowId: 1, }); } - setState(state: RawBadgeState, tabId?: number): Promise { + setState = jest.fn().mockImplementation((state: RawBadgeState, tabId?: number): Promise => { if (tabId !== undefined) { this.specificStates[tabId] = state; } else { @@ -25,7 +43,7 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi { } return Promise.resolve(); - } + }); getTabs(): Promise { return Promise.resolve(this.tabs); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index d0bdaa504b3..60a42078cfc 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -32,6 +32,15 @@ export class BrowserApi { return BrowserApi.manifestVersion === expectedVersion; } + /** + * Gets all open browser windows, including their tabs. + * + * @returns A promise that resolves to an array of browser windows. + */ + static async getWindows(): Promise { + return new Promise((resolve) => chrome.windows.getAll({ populate: true }, resolve)); + } + /** * Gets the current window or the window with the given id. * diff --git a/apps/desktop/src/key-management/electron-key.service.spec.ts b/apps/desktop/src/key-management/electron-key.service.spec.ts index af01bf51c15..2d60c47217d 100644 --- a/apps/desktop/src/key-management/electron-key.service.spec.ts +++ b/apps/desktop/src/key-management/electron-key.service.spec.ts @@ -1,10 +1,10 @@ import { mock } from "jest-mock-extended"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; diff --git a/apps/desktop/src/key-management/electron-key.service.ts b/apps/desktop/src/key-management/electron-key.service.ts index 562662f6696..59295b2ca21 100644 --- a/apps/desktop/src/key-management/electron-key.service.ts +++ b/apps/desktop/src/key-management/electron-key.service.ts @@ -1,9 +1,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 0123ea4fb57..4b6e9a431b4 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -28,6 +28,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getById } from "@bitwarden/common/platform/misc"; import { BannerModule, IconModule, AdminConsoleLogo } from "@bitwarden/components"; +import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { NonIndividualSubscriber } from "@bitwarden/web-vault/app/billing/types"; import { TaxIdWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components"; @@ -50,6 +51,7 @@ import { WebLayoutModule } from "../../../layouts/web-layout.module"; BannerModule, TaxIdWarningComponent, TaxIdWarningComponent, + OrganizationWarningsModule, ], }) export class OrganizationLayoutComponent implements OnInit { diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 3daa6c17d07..07f6be7d7f6 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -34,6 +34,8 @@ import { openChangePlanDialog, } from "../../../billing/organizations/change-plan-dialog.component"; import { EventService } from "../../../core"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { EventExportService } from "../../../tools/event-export"; import { BaseEventsComponent } from "../../common/base.events.component"; @@ -46,9 +48,8 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { }; @Component({ - selector: "app-org-events", templateUrl: "events.component.html", - standalone: false, + imports: [SharedModule, HeaderModule], }) export class EventsComponent extends BaseEventsComponent implements OnInit, OnDestroy { exportFileName = "org-events"; diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 03b77cfaa71..16543cdb58c 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -8,6 +8,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { SharedModule } from "../../../shared"; + export type UserConfirmDialogData = { name: string; userId: string; @@ -16,9 +18,8 @@ export type UserConfirmDialogData = { }; @Component({ - selector: "app-user-confirm", templateUrl: "user-confirm.component.html", - standalone: false, + imports: [SharedModule], }) export class UserConfirmComponent implements OnInit { name: string; diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index b0c89cd30ab..2ca566a0af2 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -1,12 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { Component, inject } from "@angular/core"; import { Params } from "@angular/router"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { Icons, ToastService } from "@bitwarden/components"; +import { IconModule, Icons, ToastService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; @@ -16,9 +18,8 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component"; * personal email address." - https://bitwarden.com/learning/free-families-plan-for-enterprise/ */ @Component({ - selector: "app-accept-family-sponsorship", templateUrl: "accept-family-sponsorship.component.html", - standalone: false, + imports: [CommonModule, I18nPipe, IconModule], }) export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { protected logo = Icons.BitwardenLogo; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 637e1b77ce0..d7dbdbc4ae5 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -8,10 +8,7 @@ import { import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; -import { EventsComponent as OrgEventsComponent } from "../admin-console/organizations/manage/events.component"; -import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component"; import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/manage/verify-recover-delete-org.component"; -import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; @@ -61,13 +58,10 @@ import { SharedModule } from "./shared.module"; PremiumBadgeComponent, ], declarations: [ - AcceptFamilySponsorshipComponent, - OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, OrgReusedPasswordsReportComponent, OrgUnsecuredWebsitesReportComponent, - OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, @@ -82,12 +76,10 @@ import { SharedModule } from "./shared.module"; UserVerificationModule, PremiumBadgeComponent, OrganizationLayoutComponent, - OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, OrgReusedPasswordsReportComponent, OrgUnsecuredWebsitesReportComponent, - OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, PremiumBadgeComponent, RecoverDeleteComponent, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts index 6ef5be521ed..137eadb30b5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts @@ -6,10 +6,10 @@ import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrganizationId } from "@bitwarden/common/types/guid"; diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 6edc298235d..8b080089c3c 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -12,11 +12,11 @@ import { awaitAsync, mockAccountServiceWith, } from "../../../../spec"; +import { KeyGenerationService } from "../../../key-management/crypto"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EnvironmentService } from "../../../platform/abstractions/environment.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 6e2b4391c96..2664b0d4351 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -6,10 +6,10 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from // eslint-disable-next-line no-restricted-imports import { PBKDF2KdfConfig, KeyService } from "@bitwarden/key-management"; +import { KeyGenerationService } from "../../../key-management/crypto"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/tools/state/buffered-state.ts b/libs/common/src/tools/state/buffered-state.ts index b10ee6c7b85..c53390e4af7 100644 --- a/libs/common/src/tools/state/buffered-state.ts +++ b/libs/common/src/tools/state/buffered-state.ts @@ -45,12 +45,14 @@ export class BufferedState implements SingleUserState map((dependency) => [key.shouldOverwrite(dependency), dependency] as const), ); const overwrite$ = combineLatest([hasValue$, overwriteDependency$]).pipe( - concatMap(async ([hasValue, [shouldOverwrite, dependency]]) => { - if (hasValue && shouldOverwrite) { - await this.overwriteOutput(dependency); - } - return [false, null] as const; - }), + concatMap( + async ([hasValue, [shouldOverwrite, dependency]]): Promise => { + if (hasValue && shouldOverwrite) { + await this.overwriteOutput(dependency); + } + return [false, null] as const; + }, + ), ); // drive overwrites only when there's a subscription; @@ -71,7 +73,7 @@ export class BufferedState implements SingleUserState private async overwriteOutput(dependency: Dependency) { // take the latest value from the buffer let buffered: Input; - await this.bufferedState.update((state) => { + await this.bufferedState.update((state): Input | null => { buffered = state ?? null; return null; }); diff --git a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.ts b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.ts index 41dcb94a001..02e86ad8fe0 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.ts @@ -345,7 +345,8 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic timeout({ // timeout after 1 second each: 1000, - with() { + // TODO(PM-22309): Typescript 5.8 update, confirm type + with(): any[] { return []; }, }), @@ -370,7 +371,8 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic timeout({ // timeout after 1 second each: 1000, - with() { + // TODO(PM-22309): Typescript 5.8 update, confirm type + with(): any[] { return []; }, }),