mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 13:10:17 +00:00
add exclamation badge for at risk passwords on tab
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SecurityTask, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { BadgeService } from "../../platform/badge/badge.service";
|
||||
import { BadgeStatePriority } from "../../platform/badge/priority";
|
||||
|
||||
import { AutofillBadgeUpdaterService } from "./autofill-badge-updater.service";
|
||||
|
||||
describe("AutofillBadgeUpdaterService", () => {
|
||||
let service: AutofillBadgeUpdaterService;
|
||||
|
||||
let setState: jest.Mock;
|
||||
let clearState: jest.Mock;
|
||||
let warning: jest.Mock;
|
||||
let getAllDecryptedForUrl: jest.Mock;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject({ id: "test-account-id" });
|
||||
const cipherViews$ = new BehaviorSubject([]);
|
||||
const enableBadgeCounter$ = new BehaviorSubject(true);
|
||||
const pendingTasks$ = new BehaviorSubject([]);
|
||||
|
||||
beforeEach(() => {
|
||||
setState = jest.fn().mockResolvedValue(undefined);
|
||||
clearState = jest.fn().mockResolvedValue(undefined);
|
||||
warning = jest.fn();
|
||||
getAllDecryptedForUrl = jest.fn().mockResolvedValue([]);
|
||||
|
||||
service = new AutofillBadgeUpdaterService(
|
||||
{ setState, clearState } as unknown as BadgeService,
|
||||
{ activeAccount$ } as unknown as AccountService,
|
||||
{ cipherViews$, getAllDecryptedForUrl } as unknown as CipherService,
|
||||
{ enableBadgeCounter$ } as unknown as BadgeSettingsServiceAbstraction,
|
||||
{ warning } as unknown as LogService,
|
||||
{ pendingTasks$ } as unknown as TaskService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("setTabState", () => {
|
||||
const userId = "test-user-id" as UserId;
|
||||
|
||||
it("clears the tab state when there are no ciphers and no pending tasks", async () => {
|
||||
const tab = { id: 1 } as chrome.tabs.Tab;
|
||||
|
||||
await service["setTabState"](tab, userId, true, []);
|
||||
|
||||
expect(clearState).toHaveBeenCalledWith("autofill-badge-1");
|
||||
});
|
||||
|
||||
it("clears the tab state when the enableBadgeCounter is false and no pending tasks", async () => {
|
||||
const tab = { id: 2, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||
getAllDecryptedForUrl.mockResolvedValueOnce([{ id: "cipher1" }]);
|
||||
|
||||
await service["setTabState"](tab, userId, false, []);
|
||||
|
||||
expect(clearState).toHaveBeenCalledWith("autofill-badge-2");
|
||||
});
|
||||
|
||||
it("sets state when there are pending tasks for the tab", async () => {
|
||||
const tab = { id: 3, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||
const pendingTasks: SecurityTask[] = [{ id: "task1", cipherId: "cipher1" } as SecurityTask];
|
||||
getAllDecryptedForUrl.mockResolvedValueOnce([{ id: "cipher1" }]);
|
||||
|
||||
await service["setTabState"](tab, userId, true, pendingTasks);
|
||||
|
||||
expect(setState).toHaveBeenCalledWith(
|
||||
"autofill-badge-3",
|
||||
BadgeStatePriority.High,
|
||||
{ backgroundColor: "#cb263a", text: "!" },
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets state when there are pending tasks for the tab even when badge counter is false", async () => {
|
||||
const tab = { id: 4, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||
const pendingTasks: SecurityTask[] = [{ id: "task2", cipherId: "cipher2" } as SecurityTask];
|
||||
getAllDecryptedForUrl.mockResolvedValueOnce([{ id: "cipher2" }]);
|
||||
|
||||
await service["setTabState"](tab, userId, false, pendingTasks);
|
||||
|
||||
expect(setState).toHaveBeenCalledWith(
|
||||
"autofill-badge-4",
|
||||
BadgeStatePriority.High,
|
||||
{ backgroundColor: "#cb263a", text: "!" },
|
||||
4,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets cipher count", async () => {
|
||||
const tab = { id: 5, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||
getAllDecryptedForUrl.mockResolvedValueOnce([
|
||||
{ id: "cipher2" },
|
||||
{ id: "cipher3" },
|
||||
{ id: "cipher4" },
|
||||
]);
|
||||
|
||||
await service["setTabState"](tab, userId, true, []);
|
||||
|
||||
expect(setState).toHaveBeenCalledWith(
|
||||
"autofill-badge-5",
|
||||
BadgeStatePriority.Default,
|
||||
{ text: "3" },
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets cipher count to 9+ when there are more than 9 ciphers", async () => {
|
||||
const tab = { id: 6, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||
getAllDecryptedForUrl.mockResolvedValueOnce(
|
||||
new Array(12).fill(null).map((_, i) => ({ id: `cipher-${i + 1}` })),
|
||||
);
|
||||
|
||||
await service["setTabState"](tab, userId, true, []);
|
||||
|
||||
expect(setState).toHaveBeenCalledWith(
|
||||
"autofill-badge-6",
|
||||
BadgeStatePriority.Default,
|
||||
{ text: "9+" },
|
||||
6,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import { combineLatest, distinctUntilChanged, mergeMap, of, Subject, switchMap } from "rxjs";
|
||||
import { combineLatest, distinctUntilChanged, map, mergeMap, of, Subject, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
|
||||
import { BadgeService } from "../../platform/badge/badge.service";
|
||||
import { BadgeStatePriority } from "../../platform/badge/priority";
|
||||
@@ -17,12 +19,26 @@ export class AutofillBadgeUpdaterService {
|
||||
private tabUpdated$ = new Subject<chrome.tabs.Tab>();
|
||||
private tabRemoved$ = new Subject<number>();
|
||||
|
||||
private activeAccount$ = this.accountService.activeAccount$;
|
||||
|
||||
private pendingTasks$ = this.activeAccount$.pipe(
|
||||
filterOutNullish(),
|
||||
switchMap((account) =>
|
||||
this.taskService
|
||||
.pendingTasks$(account.id)
|
||||
.pipe(
|
||||
map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private badgeService: BadgeService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private taskService: TaskService,
|
||||
) {
|
||||
const cipherViews$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => (account?.id ? this.cipherService.cipherViews$(account?.id) : of([]))),
|
||||
@@ -33,9 +49,10 @@ export class AutofillBadgeUpdaterService {
|
||||
enableBadgeCounter:
|
||||
this.badgeSettingsService.enableBadgeCounter$.pipe(distinctUntilChanged()),
|
||||
ciphers: cipherViews$,
|
||||
pendingTasks: this.pendingTasks$,
|
||||
})
|
||||
.pipe(
|
||||
mergeMap(async ({ account, enableBadgeCounter, ciphers }) => {
|
||||
mergeMap(async ({ account, enableBadgeCounter, pendingTasks }) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
@@ -46,8 +63,9 @@ export class AutofillBadgeUpdaterService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enableBadgeCounter) {
|
||||
await this.setTabState(tab, account.id);
|
||||
// When the badge counter is disabled, a tab state may be applicable based on the pending tasks.
|
||||
if (enableBadgeCounter || pendingTasks.length > 0) {
|
||||
await this.setTabState(tab, account.id, enableBadgeCounter, pendingTasks);
|
||||
} else {
|
||||
await this.clearTabState(tab.id);
|
||||
}
|
||||
@@ -61,15 +79,16 @@ export class AutofillBadgeUpdaterService {
|
||||
enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$,
|
||||
replaced: this.tabReplaced$,
|
||||
ciphers: cipherViews$,
|
||||
pendingTasks: this.pendingTasks$,
|
||||
})
|
||||
.pipe(
|
||||
mergeMap(async ({ account, enableBadgeCounter, replaced }) => {
|
||||
if (!account || !enableBadgeCounter) {
|
||||
mergeMap(async ({ account, enableBadgeCounter, replaced, pendingTasks }) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.clearTabState(replaced.removedTabId);
|
||||
await this.setTabState(replaced.addedTab, account.id);
|
||||
await this.setTabState(replaced.addedTab, account.id, enableBadgeCounter, pendingTasks);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
@@ -79,14 +98,15 @@ export class AutofillBadgeUpdaterService {
|
||||
enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$,
|
||||
tab: this.tabUpdated$,
|
||||
ciphers: cipherViews$,
|
||||
pendingTasks: this.pendingTasks$,
|
||||
})
|
||||
.pipe(
|
||||
mergeMap(async ({ account, enableBadgeCounter, tab }) => {
|
||||
if (!account || !enableBadgeCounter) {
|
||||
mergeMap(async ({ account, enableBadgeCounter, tab, pendingTasks }) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setTabState(tab, account.id);
|
||||
await this.setTabState(tab, account.id, enableBadgeCounter, pendingTasks);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
@@ -132,7 +152,12 @@ export class AutofillBadgeUpdaterService {
|
||||
BrowserApi.addListener(chrome.tabs.onRemoved, (tabId, _) => this.tabRemoved$.next(tabId));
|
||||
}
|
||||
|
||||
private async setTabState(tab: chrome.tabs.Tab, userId: UserId) {
|
||||
private async setTabState(
|
||||
tab: chrome.tabs.Tab,
|
||||
userId: UserId,
|
||||
enableBadgeCounter: boolean,
|
||||
pendingTasks?: SecurityTask[],
|
||||
) {
|
||||
if (!tab.id) {
|
||||
this.logService.warning("Tab event received but tab id is undefined");
|
||||
return;
|
||||
@@ -141,11 +166,30 @@ export class AutofillBadgeUpdaterService {
|
||||
const ciphers = tab.url ? await this.cipherService.getAllDecryptedForUrl(tab.url, userId) : [];
|
||||
const cipherCount = ciphers.length;
|
||||
|
||||
if (cipherCount === 0) {
|
||||
const hasPendingTasksForTab = (pendingTasks ?? []).some((task) =>
|
||||
ciphers.some((cipher) => cipher.id === task.cipherId && !cipher.isDeleted),
|
||||
);
|
||||
|
||||
const skipBadgeUpdate = !enableBadgeCounter && !hasPendingTasksForTab;
|
||||
|
||||
if (cipherCount === 0 || skipBadgeUpdate) {
|
||||
await this.clearTabState(tab.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPendingTasksForTab) {
|
||||
await this.badgeService.setState(
|
||||
StateName(tab.id),
|
||||
BadgeStatePriority.High,
|
||||
{
|
||||
text: "!",
|
||||
backgroundColor: "#cb263a", // equivalent to danger-600
|
||||
},
|
||||
tab.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const countText = cipherCount > 9 ? "9+" : cipherCount.toString();
|
||||
await this.badgeService.setState(
|
||||
StateName(tab.id),
|
||||
|
||||
@@ -1820,6 +1820,7 @@ export default class MainBackground {
|
||||
this.cipherService,
|
||||
this.badgeSettingsService,
|
||||
this.logService,
|
||||
this.taskService,
|
||||
);
|
||||
|
||||
this.tabsBackground = new TabsBackground(
|
||||
|
||||
Reference in New Issue
Block a user