mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 18:33:50 +00:00
merge main
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
<bit-form-control
|
||||
class="tw-pl-5"
|
||||
[disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)"
|
||||
*ngIf="this.form.value.biometric && showAutoPrompt"
|
||||
*ngIf="this.form.value.biometric"
|
||||
>
|
||||
<input
|
||||
bitCheckbox
|
||||
|
||||
@@ -30,7 +30,6 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
@@ -110,7 +109,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
hasVaultTimeoutPolicy = false;
|
||||
biometricUnavailabilityReason: string;
|
||||
showChangeMasterPass = true;
|
||||
showAutoPrompt = true;
|
||||
pinEnabled$: Observable<boolean> = of(true);
|
||||
|
||||
form = this.formBuilder.group({
|
||||
@@ -147,11 +145,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// Firefox popup closes when unfocused by biometrics, blocking all unlock methods
|
||||
if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) {
|
||||
this.showAutoPrompt = false;
|
||||
}
|
||||
|
||||
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
|
||||
const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe(
|
||||
|
||||
@@ -191,6 +191,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DefaultEndUserNotificationService,
|
||||
EndUserNotificationService,
|
||||
} from "@bitwarden/common/vault/notifications";
|
||||
import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
@@ -403,6 +407,7 @@ export default class MainBackground {
|
||||
sdkService: SdkService;
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
endUserNotificationService: EndUserNotificationService;
|
||||
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
||||
taskService: TaskService;
|
||||
|
||||
@@ -1329,6 +1334,14 @@ export default class MainBackground {
|
||||
|
||||
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
|
||||
this.ipcService = new IpcBackgroundService(this.logService);
|
||||
|
||||
this.endUserNotificationService = new DefaultEndUserNotificationService(
|
||||
this.stateProvider,
|
||||
this.apiService,
|
||||
this.notificationsService,
|
||||
this.authService,
|
||||
this.logService,
|
||||
);
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
@@ -1415,6 +1428,9 @@ export default class MainBackground {
|
||||
this.taskService.listenForTaskNotifications();
|
||||
}
|
||||
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) {
|
||||
this.endUserNotificationService.listenForEndUserNotifications();
|
||||
}
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
} from "@bitwarden/key-management";
|
||||
import { UnlockOptions } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";
|
||||
|
||||
import { ExtensionLockComponentService } from "./extension-lock-component.service";
|
||||
@@ -117,6 +119,62 @@ describe("ExtensionLockComponentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("popOutBrowserExtension", () => {
|
||||
let openPopoutSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
openPopoutSpy = jest
|
||||
.spyOn(BrowserPopupUtils, "openCurrentPagePopout")
|
||||
.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("opens pop-out when the current window is neither a pop-out nor a sidebar", async () => {
|
||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||
jest.spyOn(BrowserPopupUtils, "inSidebar").mockReturnValue(false);
|
||||
|
||||
await service.popOutBrowserExtension();
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(global.window);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[true, false],
|
||||
[false, true],
|
||||
[true, true],
|
||||
])("should not open pop-out under other conditions.", async (inPopout, inSidebar) => {
|
||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(inPopout);
|
||||
jest.spyOn(BrowserPopupUtils, "inSidebar").mockReturnValue(inSidebar);
|
||||
|
||||
await service.popOutBrowserExtension();
|
||||
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeBrowserExtensionPopout", () => {
|
||||
let closePopupSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
closePopupSpy = jest.spyOn(BrowserApi, "closePopup").mockReturnValue();
|
||||
});
|
||||
|
||||
it("closes pop-out when in pop-out", () => {
|
||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
||||
|
||||
service.closeBrowserExtensionPopout();
|
||||
|
||||
expect(closePopupSpy).toHaveBeenCalledWith(global.window);
|
||||
});
|
||||
|
||||
it("doesn't close pop-out when not in pop-out", () => {
|
||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||
|
||||
service.closeBrowserExtensionPopout();
|
||||
|
||||
expect(closePopupSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowVisible", () => {
|
||||
it("throws an error", async () => {
|
||||
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import { LockComponentService, UnlockOptions } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";
|
||||
|
||||
export class ExtensionLockComponentService implements LockComponentService {
|
||||
@@ -37,6 +39,18 @@ export class ExtensionLockComponentService implements LockComponentService {
|
||||
return biometricsError.description;
|
||||
}
|
||||
|
||||
async popOutBrowserExtension(): Promise<void> {
|
||||
if (!BrowserPopupUtils.inPopout(global.window) && !BrowserPopupUtils.inSidebar(global.window)) {
|
||||
await BrowserPopupUtils.openCurrentPagePopout(global.window);
|
||||
}
|
||||
}
|
||||
|
||||
closeBrowserExtensionPopout(): void {
|
||||
if (BrowserPopupUtils.inPopout(global.window)) {
|
||||
BrowserApi.closePopup(global.window);
|
||||
}
|
||||
}
|
||||
|
||||
async isWindowVisible(): Promise<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@@ -12,10 +12,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { EndUserNotificationService } from "@bitwarden/common/vault/notifications";
|
||||
import { NotificationView } from "@bitwarden/common/vault/notifications/models";
|
||||
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
@@ -66,6 +69,7 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
let mockTasks$: BehaviorSubject<SecurityTask[]>;
|
||||
let mockCiphers$: BehaviorSubject<CipherView[]>;
|
||||
let mockOrgs$: BehaviorSubject<Organization[]>;
|
||||
let mockNotifications$: BehaviorSubject<NotificationView[]>;
|
||||
let mockInlineMenuVisibility$: BehaviorSubject<InlineMenuVisibilitySetting>;
|
||||
let calloutDismissed$: BehaviorSubject<boolean>;
|
||||
const setInlineMenuVisibility = jest.fn();
|
||||
@@ -73,6 +77,7 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
const mockAtRiskPasswordPageService = mock<AtRiskPasswordPageService>();
|
||||
const mockChangeLoginPasswordService = mock<ChangeLoginPasswordService>();
|
||||
const mockDialogService = mock<DialogService>();
|
||||
const mockConfigService = mock<ConfigService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
mockTasks$ = new BehaviorSubject<SecurityTask[]>([
|
||||
@@ -101,6 +106,7 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
name: "Org 1",
|
||||
} as Organization,
|
||||
]);
|
||||
mockNotifications$ = new BehaviorSubject<NotificationView[]>([]);
|
||||
|
||||
mockInlineMenuVisibility$ = new BehaviorSubject<InlineMenuVisibilitySetting>(
|
||||
AutofillOverlayVisibility.Off,
|
||||
@@ -110,6 +116,7 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
setInlineMenuVisibility.mockClear();
|
||||
mockToastService.showToast.mockClear();
|
||||
mockDialogService.open.mockClear();
|
||||
mockConfigService.getFeatureFlag.mockClear();
|
||||
mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -133,6 +140,12 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
cipherViews$: () => mockCiphers$,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: EndUserNotificationService,
|
||||
useValue: {
|
||||
unreadNotifications$: () => mockNotifications$,
|
||||
},
|
||||
},
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: AccountService, useValue: { activeAccount$: of({ id: "user" }) } },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
@@ -145,6 +158,7 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
},
|
||||
},
|
||||
{ provide: ToastService, useValue: mockToastService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
})
|
||||
.overrideModule(JslibModule, {
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, OnInit, signal } from "@angular/core";
|
||||
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs";
|
||||
import {
|
||||
combineLatest,
|
||||
concat,
|
||||
concatMap,
|
||||
firstValueFrom,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -11,10 +23,13 @@ import {
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { EndUserNotificationService } from "@bitwarden/common/vault/notifications";
|
||||
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import {
|
||||
@@ -81,6 +96,9 @@ export class AtRiskPasswordsComponent implements OnInit {
|
||||
private changeLoginPasswordService = inject(ChangeLoginPasswordService);
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
private dialogService = inject(DialogService);
|
||||
private endUserNotificationService = inject(EndUserNotificationService);
|
||||
private configService = inject(ConfigService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
/**
|
||||
* The cipher that is currently being launched. Used to show a loading spinner on the badge button.
|
||||
@@ -180,6 +198,36 @@ export class AtRiskPasswordsComponent implements OnInit {
|
||||
await this.atRiskPasswordPageService.dismissGettingStarted(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) {
|
||||
this.markTaskNotificationsAsRead();
|
||||
}
|
||||
}
|
||||
|
||||
private markTaskNotificationsAsRead() {
|
||||
this.activeUserData$
|
||||
.pipe(
|
||||
switchMap(({ tasks, userId }) => {
|
||||
return this.endUserNotificationService.unreadNotifications$(userId).pipe(
|
||||
take(1),
|
||||
map((notifications) => {
|
||||
return notifications.filter((notification) => {
|
||||
return tasks.some((task) => task.id === notification.taskId);
|
||||
});
|
||||
}),
|
||||
concatMap((unreadTaskNotifications) => {
|
||||
// TODO: Investigate creating a bulk endpoint to mark notifications as read
|
||||
return concat(
|
||||
...unreadTaskNotifications.map((n) =>
|
||||
this.endUserNotificationService.markAsRead(n.id, userId),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async viewCipher(cipher: CipherView) {
|
||||
|
||||
@@ -104,6 +104,22 @@ describe("DesktopLockComponentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("popOutBrowserExtension", () => {
|
||||
it("throws platform not supported error", () => {
|
||||
expect(() => service.popOutBrowserExtension()).toThrow(
|
||||
"Method not supported on this platform.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeBrowserExtensionPopout", () => {
|
||||
it("throws platform not supported error", () => {
|
||||
expect(() => service.closeBrowserExtensionPopout()).toThrow(
|
||||
"Method not supported on this platform.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowVisible", () => {
|
||||
it("returns the window visibility", async () => {
|
||||
isWindowVisibleMock.mockReturnValue(true);
|
||||
|
||||
@@ -27,6 +27,14 @@ export class DesktopLockComponentService implements LockComponentService {
|
||||
return null;
|
||||
}
|
||||
|
||||
popOutBrowserExtension(): Promise<void> {
|
||||
throw new Error("Method not supported on this platform.");
|
||||
}
|
||||
|
||||
closeBrowserExtensionPopout(): void {
|
||||
throw new Error("Method not supported on this platform.");
|
||||
}
|
||||
|
||||
async isWindowVisible(): Promise<boolean> {
|
||||
return ipc.platform.isWindowVisible();
|
||||
}
|
||||
|
||||
@@ -52,6 +52,22 @@ describe("WebLockComponentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("popOutBrowserExtension", () => {
|
||||
it("throws platform not supported error", () => {
|
||||
expect(() => service.popOutBrowserExtension()).toThrow(
|
||||
"Method not supported on this platform.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeBrowserExtensionPopout", () => {
|
||||
it("throws platform not supported error", () => {
|
||||
expect(() => service.closeBrowserExtensionPopout()).toThrow(
|
||||
"Method not supported on this platform.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowVisible", () => {
|
||||
it("throws an error", async () => {
|
||||
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
|
||||
|
||||
@@ -24,6 +24,14 @@ export class WebLockComponentService implements LockComponentService {
|
||||
return null;
|
||||
}
|
||||
|
||||
popOutBrowserExtension(): Promise<void> {
|
||||
throw new Error("Method not supported on this platform.");
|
||||
}
|
||||
|
||||
closeBrowserExtensionPopout(): void {
|
||||
throw new Error("Method not supported on this platform.");
|
||||
}
|
||||
|
||||
async isWindowVisible(): Promise<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user