mirror of
https://github.com/bitwarden/browser
synced 2026-02-14 15:33:55 +00:00
Merge branch 'main' into auth/pm-3387/invalid-auth-request-error
This commit is contained in:
@@ -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<MutationRecord>;
|
||||
let mockBodyMutationRecord: MockProxy<MutationRecord>;
|
||||
let mockHTMLMutationRecord: MockProxy<MutationRecord>;
|
||||
let buttonElement: HTMLElement;
|
||||
let listElement: HTMLElement;
|
||||
let isInlineMenuListVisibleSpy: jest.SpyInstance;
|
||||
@@ -329,6 +331,16 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
<div class="overlay-list"></div>
|
||||
`;
|
||||
mockMutationRecord = mock<MutationRecord>({ target: globalThis.document.body } as any);
|
||||
mockHTMLMutationRecord = mock<MutationRecord>({
|
||||
target: globalThis.document.body.parentElement,
|
||||
attributeName: "style",
|
||||
type: "attributes",
|
||||
} as any);
|
||||
mockBodyMutationRecord = mock<MutationRecord>({
|
||||
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 = "";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<chrome.tabs.TabActiveInfo | undefined>;
|
||||
// activeTabs$: Observable<chrome.tabs.Tab[]>;
|
||||
|
||||
setState(state: RawBadgeState, tabId?: number): Promise<void>;
|
||||
getTabs(): Promise<number[]>;
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]>;
|
||||
}
|
||||
|
||||
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<chrome.tabs.Tab[]> {
|
||||
return BrowserApi.getActiveTabs();
|
||||
}
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async setState(state: RawBadgeState, tabId?: number): Promise<void> {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Record<string, StateSetting>>;
|
||||
private serviceState: GlobalState<Record<string, StateSetting>>;
|
||||
|
||||
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<StateSetting>, 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<string, StateSetting> | 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,15 +9,33 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi {
|
||||
specificStates: Record<number, RawBadgeState> = {};
|
||||
generalState?: RawBadgeState;
|
||||
tabs: number[] = [];
|
||||
activeTabs: number[] = [];
|
||||
|
||||
setActiveTab(tabId: number) {
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
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<void> {
|
||||
setState = jest.fn().mockImplementation((state: RawBadgeState, tabId?: number): Promise<void> => {
|
||||
if (tabId !== undefined) {
|
||||
this.specificStates[tabId] = state;
|
||||
} else {
|
||||
@@ -25,7 +43,7 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi {
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
getTabs(): Promise<number[]> {
|
||||
return Promise.resolve(this.tabs);
|
||||
|
||||
@@ -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<chrome.windows.Window[]> {
|
||||
return new Promise((resolve) => chrome.windows.getAll({ populate: true }, resolve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current window or the window with the given id.
|
||||
*
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<EventSystemUser, string> = {
|
||||
};
|
||||
|
||||
@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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -45,12 +45,14 @@ export class BufferedState<Input, Output, Dependency> 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<readonly [false, null]> => {
|
||||
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<Input, Output, Dependency> 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;
|
||||
});
|
||||
|
||||
@@ -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 [];
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user