diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index c19505118b..1e4d0c3677 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -17,8 +17,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import BrowserPlatformUtilsService from "../../platform/services/browser-platform-utils.service"; import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillService } from "../services/abstractions/autofill.service"; import { createAutofillPageDetailsMock, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7512093a2d..9d5f6943ec 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -198,10 +198,11 @@ import { BrowserEnvironmentService } from "../platform/services/browser-environm import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service"; -import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserStateService } from "../platform/services/browser-state.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; +import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; +import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserSendService } from "../services/browser-send.service"; @@ -438,28 +439,10 @@ export default class MainBackground { migrationRunner, ); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); - this.platformUtilsService = new BrowserPlatformUtilsService( + this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, - (clipboardValue, clearMs) => { - if (this.systemService != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.systemService.clearClipboard(clipboardValue, clearMs); - } - }, - async () => { - if (this.nativeMessagingBackground != null) { - const promise = this.nativeMessagingBackground.getResponse(); - - try { - await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); - } catch (e) { - return Promise.reject(e); - } - - return promise.then((result) => result.response === "unlocked"); - } - }, + (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), + async () => this.biometricUnlock(), self, ); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); @@ -1222,6 +1205,23 @@ export default class MainBackground { } } + async clearClipboard(clipboardValue: string, clearMs: number) { + if (this.systemService != null) { + await this.systemService.clearClipboard(clipboardValue, clearMs); + } + } + + async biometricUnlock(): Promise { + if (this.nativeMessagingBackground == null) { + return false; + } + + const responsePromise = this.nativeMessagingBackground.getResponse(); + await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); + const response = await responsePromise; + return response.response === "unlocked"; + } + private async fullSync(override = false) { const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const lastSync = await this.syncService.getLastSync(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 2717a7b2b5..620f9735c8 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -122,7 +122,7 @@ export class NativeMessagingBackground { break; case "disconnected": if (this.connecting) { - reject("startDesktop"); + reject(new Error("startDesktop")); } this.connected = false; this.port.disconnect(); @@ -203,7 +203,7 @@ export class NativeMessagingBackground { this.connected = false; const reason = error != null ? "desktopIntegrationDisabled" : null; - reject(reason); + reject(new Error(reason)); }); }); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 88d0e3f90a..6c0c0f169a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -19,7 +19,7 @@ import { AutofillService } from "../autofill/services/abstractions/autofill.serv import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; -import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; +import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { AbortManager } from "../vault/background/abort-manager"; import { Fido2Service } from "../vault/services/abstractions/fido2.service"; @@ -68,6 +68,7 @@ export default class RuntimeBackground { "checkFido2FeatureEnabled", "fido2RegisterCredentialRequest", "fido2GetCredentialRequest", + "biometricUnlock", ]; if (messagesWithResponse.includes(msg.command)) { @@ -305,6 +306,14 @@ export default class RuntimeBackground { ); case "switchAccount": { await this.main.switchAccount(msg.userId); + break; + } + case "clearClipboard": { + await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs); + break; + } + case "biometricUnlock": { + return await this.main.biometricUnlock(); } } } diff --git a/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts b/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts index d3ac465e4c..6f46d87418 100644 --- a/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts @@ -1,6 +1,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import BrowserPlatformUtilsService from "../../services/browser-platform-utils.service"; +import { BackgroundPlatformUtilsService } from "../../services/platform-utils/background-platform-utils.service"; import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory"; @@ -25,7 +25,7 @@ export function platformUtilsServiceFactory( "platformUtilsService", opts, async () => - new BrowserPlatformUtilsService( + new BackgroundPlatformUtilsService( await messagingServiceFactory(cache, opts), opts.platformUtilsServiceOptions.clipboardWriteCallback, opts.platformUtilsServiceOptions.biometricCallback, diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 5e490a99f4..362aac1af9 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -3,7 +3,7 @@ import { Observable } from "rxjs"; import { DeviceType } from "@bitwarden/common/enums"; import { TabMessage } from "../../types/tab-messages"; -import BrowserPlatformUtilsService from "../services/browser-platform-utils.service"; +import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; diff --git a/apps/browser/src/platform/listeners/update-badge.ts b/apps/browser/src/platform/listeners/update-badge.ts index ee9996a28b..c39cb8c894 100644 --- a/apps/browser/src/platform/listeners/update-badge.ts +++ b/apps/browser/src/platform/listeners/update-badge.ts @@ -17,7 +17,7 @@ import { Account } from "../../models/account"; import IconDetails from "../../vault/background/models/icon-details"; import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory"; import { BrowserApi } from "../browser/browser-api"; -import BrowserPlatformUtilsService from "../services/browser-platform-utils.service"; +import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; export type BadgeOptions = { tab?: chrome.tabs.Tab; diff --git a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts new file mode 100644 index 0000000000..27ed3f016b --- /dev/null +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -0,0 +1,28 @@ +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; + +export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService { + constructor( + private messagingService: MessagingService, + clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, + biometricCallback: () => Promise, + win: Window & typeof globalThis, + ) { + super(clipboardWriteCallback, biometricCallback, win); + } + + override showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any, + ): void { + this.messagingService.send("showToast", { + text: text, + title: title, + type: type, + options: options, + }); + } +} diff --git a/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts similarity index 94% rename from apps/browser/src/platform/services/browser-platform-utils.service.spec.ts rename to apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index e70a641624..cf6816f405 100644 --- a/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -1,11 +1,26 @@ import { DeviceType } from "@bitwarden/common/enums"; -import { flushPromises } from "../../autofill/spec/testing-utils"; -import { SafariApp } from "../../browser/safariApp"; -import { BrowserApi } from "../browser/browser-api"; +import { flushPromises } from "../../../autofill/spec/testing-utils"; +import { SafariApp } from "../../../browser/safariApp"; +import { BrowserApi } from "../../browser/browser-api"; +import BrowserClipboardService from "../browser-clipboard.service"; -import BrowserClipboardService from "./browser-clipboard.service"; -import BrowserPlatformUtilsService from "./browser-platform-utils.service"; +import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; + +class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { + constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) { + super(clipboardSpy, null, win); + } + + showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any, + ): void { + throw new Error("Method not implemented."); + } +} describe("Browser Utils Service", () => { let browserPlatformUtilsService: BrowserPlatformUtilsService; @@ -13,10 +28,8 @@ describe("Browser Utils Service", () => { beforeEach(() => { (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); - browserPlatformUtilsService = new BrowserPlatformUtilsService( - null, + browserPlatformUtilsService = new TestBrowserPlatformUtilsService( clipboardWriteCallbackSpy, - null, window, ); }); diff --git a/apps/browser/src/platform/services/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts similarity index 94% rename from apps/browser/src/platform/services/browser-platform-utils.service.ts rename to apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index a9841db958..e9f7f17d9b 100644 --- a/apps/browser/src/platform/services/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -1,20 +1,17 @@ import { ClientType, DeviceType } from "@bitwarden/common/enums"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ClipboardOptions, PlatformUtilsService, } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SafariApp } from "../../browser/safariApp"; -import { BrowserApi } from "../browser/browser-api"; +import { SafariApp } from "../../../browser/safariApp"; +import { BrowserApi } from "../../browser/browser-api"; +import BrowserClipboardService from "../browser-clipboard.service"; -import BrowserClipboardService from "./browser-clipboard.service"; - -export default class BrowserPlatformUtilsService implements PlatformUtilsService { +export abstract class BrowserPlatformUtilsService implements PlatformUtilsService { private static deviceCache: DeviceType = null; constructor( - private messagingService: MessagingService, private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private biometricCallback: () => Promise, private globalContext: Window | ServiceWorkerGlobalScope, @@ -193,19 +190,12 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return true; } - showToast( + abstract showToast( type: "error" | "success" | "warning" | "info", title: string, text: string | string[], options?: any, - ): void { - this.messagingService.send("showToast", { - text: text, - title: title, - type: type, - options: options, - }); - } + ): void; isDev(): boolean { return process.env.ENV === "development"; @@ -279,11 +269,10 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService async supportsBiometric() { const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === "android") { - return false; + if (platformInfo.os === "mac" || platformInfo.os === "win") { + return true; } - - return true; + return false; } authenticateBiometric() { diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts new file mode 100644 index 0000000000..8cf1a8d3e4 --- /dev/null +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -0,0 +1,40 @@ +import { SecurityContext } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; +import { ToastrService } from "ngx-toastr"; + +import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; + +export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { + constructor( + private sanitizer: DomSanitizer, + private toastrService: ToastrService, + clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, + biometricCallback: () => Promise, + win: Window & typeof globalThis, + ) { + super(clipboardWriteCallback, biometricCallback, win); + } + + override showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any, + ): void { + if (typeof text === "string") { + // Already in the correct format + } else if (text.length === 1) { + text = text[0]; + } else { + let message = ""; + text.forEach( + (t: string) => + (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), + ); + text = message; + options.enableHtml = true; + } + this.toastrService.show(text, title, options, "toast-" + type); + // noop + } +} diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index e1ec0678d3..aec8ba7c66 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,26 +1,17 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, - SecurityContext, -} from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; +import { ToastrService } from "ngx-toastr"; import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; +import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; import { routerTransition } from "./app-routing.animations"; import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component"; @@ -46,11 +37,9 @@ export class AppComponent implements OnInit, OnDestroy { private i18nService: I18nService, private router: Router, private stateService: BrowserStateService, - private messagingService: MessagingService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private sanitizer: DomSanitizer, - private platformUtilsService: PlatformUtilsService, + private platformUtilsService: ForegroundPlatformUtilsService, private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, ) {} @@ -217,31 +206,7 @@ export class AppComponent implements OnInit, OnDestroy { } private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); + this.platformUtilsService.showToast(msg.type, msg.title, msg.text, msg.options); } private async showDialog(msg: SimpleDialogOptions) { diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index 05ecfb43e5..c98e30e6bc 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -1,7 +1,7 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; +import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; require("./scss/popup.scss"); require("./scss/tailwind.css"); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 5c2699c9b6..332a1db746 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,4 +1,6 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; +import { ToastrService } from "ngx-toastr"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service"; @@ -117,6 +119,7 @@ import BrowserMessagingPrivateModePopupService from "../../platform/services/bro import BrowserMessagingService from "../../platform/services/browser-messaging.service"; import { BrowserStateService } from "../../platform/services/browser-state.service"; import I18nService from "../../platform/services/i18n.service"; +import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { BrowserSendService } from "../../services/browser-send.service"; @@ -290,8 +293,32 @@ function getBgService(service: keyof MainBackground) { }, { provide: PlatformUtilsService, - useFactory: getBgService("platformUtilsService"), - deps: [], + useExisting: ForegroundPlatformUtilsService, + }, + { + provide: ForegroundPlatformUtilsService, + useClass: ForegroundPlatformUtilsService, + useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + return new ForegroundPlatformUtilsService( + sanitizer, + toastrService, + (clipboardValue: string, clearMs: number) => { + void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); + }, + async () => { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlock"); + if (!response.result) { + throw response.error; + } + return response.result; + }, + window, + ); + }, + deps: [DomSanitizer, ToastrService], }, { provide: PasswordStrengthServiceAbstraction, diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index c83c7b5e72..2fcd258885 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -397,7 +397,7 @@ export class SettingsComponent implements OnInit { // Handle connection errors this.form.controls.biometric.setValue(false); - const error = BiometricErrors[e as BiometricErrorTypes]; + const error = BiometricErrors[e.message as BiometricErrorTypes]; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises