diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index 09f03d2ef8e..17c83b7e976 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -9,44 +9,44 @@ import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-short export class MainDesktopAutotypeService { autotypeKeyboardShortcut: AutotypeKeyboardShortcut; + private isInitialized: boolean = false; constructor( private logService: LogService, private windowMain: WindowMain, ) { this.autotypeKeyboardShortcut = new AutotypeKeyboardShortcut(); + + ipcMain.handle("autofill.initAutotype", () => { + this.init(); + }); + + ipcMain.handle("autofill.autotypeIsInitialized", () => { + return this.isInitialized; + }); } init() { - ipcMain.on("autofill.configureAutotype", (event, data) => { - if (data.enabled) { - const newKeyboardShortcut = new AutotypeKeyboardShortcut(); - const newKeyboardShortcutIsValid = newKeyboardShortcut.set(data.keyboardShortcut); - - if (newKeyboardShortcutIsValid) { - this.disableAutotype(); - this.autotypeKeyboardShortcut = newKeyboardShortcut; - this.enableAutotype(); - } else { - this.logService.error( - "Attempting to configure autotype but the shortcut given is invalid.", - ); - } + ipcMain.on("autofill.toggleAutotype", (event, data) => { + if (data.enable) { + this.enableAutotype(); } else { this.disableAutotype(); - - // Deregister the incoming keyboard shortcut if needed - const setCorrectly = this.autotypeKeyboardShortcut.set(data.keyboardShortcut); - if ( - setCorrectly && - globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat()) - ) { - globalShortcut.unregister(this.autotypeKeyboardShortcut.getElectronFormat()); - this.logService.info("Autotype disabled."); - } } }); + ipcMain.on("autofill.configureAutotype", (event, data) => { + const newKeyboardShortcut = new AutotypeKeyboardShortcut(); + const newKeyboardShortcutIsValid = newKeyboardShortcut.set(data.keyboardShortcut); + + if (!newKeyboardShortcutIsValid) { + this.logService.error("Configure autotype failed: the keyboard shortcut is invalid."); + return; + } + + this.setKeyboardShortcut(newKeyboardShortcut); + }); + ipcMain.on("autofill.completeAutotypeRequest", (event, data) => { const { response } = data; @@ -61,18 +61,30 @@ export class MainDesktopAutotypeService { ); } }); + + this.isInitialized = true; } - disableAutotype() { - // Deregister the current keyboard shortcut if needed + // Deregister the keyboard shortcut if registered. + private disableAutotype() { const formattedKeyboardShortcut = this.autotypeKeyboardShortcut.getElectronFormat(); + if (globalShortcut.isRegistered(formattedKeyboardShortcut)) { globalShortcut.unregister(formattedKeyboardShortcut); - this.logService.info("Autotype disabled."); + this.logService.debug("Autotype disabled."); + } else { + this.logService.debug("Autotype is not registered, implicitly disabled."); } } + // Register the current keyboard shortcut if not already registered. private enableAutotype() { + const formattedKeyboardShortcut = this.autotypeKeyboardShortcut.getElectronFormat(); + if (globalShortcut.isRegistered(formattedKeyboardShortcut)) { + this.logService.debug("Autotype is already enabled with the keyboard shortcut."); + return; + } + const result = globalShortcut.register( this.autotypeKeyboardShortcut.getElectronFormat(), () => { @@ -85,8 +97,31 @@ export class MainDesktopAutotypeService { ); result - ? this.logService.info("Autotype enabled.") - : this.logService.info("Enabling autotype failed."); + ? this.logService.debug("Autotype enabled.") + : this.logService.error("Failed to enable Autotype."); + } + + // Set the keyboard shortcut if it differs from the present one. If + // the keyboard shortcut is set, de-register the old shortcut first. + private setKeyboardShortcut(keyboardShortcut: AutotypeKeyboardShortcut) { + if ( + keyboardShortcut.getElectronFormat() !== this.autotypeKeyboardShortcut.getElectronFormat() + ) { + const registered = globalShortcut.isRegistered( + this.autotypeKeyboardShortcut.getElectronFormat(), + ); + if (registered) { + this.disableAutotype(); + } + this.autotypeKeyboardShortcut = keyboardShortcut; + if (registered) { + this.enableAutotype(); + } + } else { + this.logService.debug( + "configureAutotype called but keyboard shortcut is not different from current.", + ); + } } private doAutotype(username: string, password: string, keyboardShortcut: string[]) { diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index fcb2f646743..4709b63cc26 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -127,8 +127,17 @@ export default { }, ); }, - configureAutotype: (enabled: boolean, keyboardShortcut: string[]) => { - ipcRenderer.send("autofill.configureAutotype", { enabled, keyboardShortcut }); + initAutotype: () => { + return ipcRenderer.invoke("autofill.initAutotype"); + }, + autotypeIsInitialized: () => { + return ipcRenderer.invoke("autofill.autotypeIsInitialized"); + }, + configureAutotype: (keyboardShortcut: string[]) => { + ipcRenderer.send("autofill.configureAutotype", { keyboardShortcut }); + }, + toggleAutotype: (enable: boolean) => { + ipcRenderer.send("autofill.toggleAutotype", { enable }); }, listenAutotypeRequest: ( fn: ( diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 24ec3907a62..225307ea158 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -1,4 +1,14 @@ -import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; +import { + combineLatest, + concatMap, + filter, + firstValueFrom, + map, + Observable, + of, + switchMap, + withLatestFrom, +} from "rxjs"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -18,6 +28,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { UserId } from "@bitwarden/user-core"; import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; +import { LogService } from "@bitwarden/logging"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; export const defaultWindowsAutotypeKeyboardShortcut: string[] = ["Control", "Shift", "B"]; @@ -46,8 +58,10 @@ export class DesktopAutotypeService { AUTOTYPE_KEYBOARD_SHORTCUT, ); + // The enabled/disabled state from the user settings menu autotypeEnabledUserSetting$: Observable = of(false); - resolvedAutotypeEnabled$: Observable = of(false); + + // The keyboard shortcut from the user settings menu autotypeKeyboardShortcut$: Observable = of(defaultWindowsAutotypeKeyboardShortcut); constructor( @@ -60,6 +74,14 @@ export class DesktopAutotypeService { private billingAccountProfileStateService: BillingAccountProfileStateService, private desktopAutotypePolicy: DesktopAutotypeDefaultSettingPolicy, ) { + this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$.pipe( + map((enabled) => enabled ?? false), + ); + + this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$.pipe( + map((shortcut) => shortcut ?? defaultWindowsAutotypeKeyboardShortcut), + ); + ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => { const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle); const firstCipher = possibleCiphers?.at(0); @@ -72,66 +94,70 @@ export class DesktopAutotypeService { } async init() { - this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; - this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$.pipe( - map((shortcut) => shortcut ?? defaultWindowsAutotypeKeyboardShortcut), - ); - // Currently Autotype is only supported for Windows - if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) { - // If `autotypeDefaultPolicy` is `true` for a user's organization, and the - // user has never changed their local autotype setting (`autotypeEnabledState`), - // we set their local setting to `true` (once the local user setting is changed - // by this policy or the user themselves, the default policy should - // never change the user setting again). - combineLatest([ - this.autotypeEnabledState.state$, - this.desktopAutotypePolicy.autotypeDefaultSetting$, - ]) - .pipe( - map(async ([autotypeEnabledState, autotypeDefaultPolicy]) => { - if (autotypeDefaultPolicy === true && autotypeEnabledState === null) { - await this.setAutotypeEnabledState(true); - } - }), - ) - .subscribe(); - - // autotypeEnabledUserSetting$ publicly represents the value the - // user has set for autotyeEnabled in their local settings. - this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; - - // resolvedAutotypeEnabled$ represents the final determination if the Autotype - // feature should be on or off. - this.resolvedAutotypeEnabled$ = combineLatest([ - this.autotypeEnabledState.state$, - this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype), - this.accountService.activeAccount$.pipe( - map((activeAccount) => activeAccount?.id), - switchMap((userId) => this.authService.authStatusFor$(userId)), - ), - this.accountService.activeAccount$.pipe( - filter((account): account is Account => !!account), - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), - ), - ), - ]).pipe( - map( - ([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus, hasPremium]) => - autotypeEnabled && - windowsDesktopAutotypeFeatureFlag && - authStatus == AuthenticationStatus.Unlocked && - hasPremium, - ), - ); - - combineLatest([this.resolvedAutotypeEnabled$, this.autotypeKeyboardShortcut$]).subscribe( - ([resolvedAutotypeEnabled, autotypeKeyboardShortcut]) => { - ipc.autofill.configureAutotype(resolvedAutotypeEnabled, autotypeKeyboardShortcut); - }, - ); + if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) { + return; } + + if (!(await ipc.autofill.autotypeIsInitialized())) { + await ipc.autofill.initAutotype(); + } + + // If `autotypeDefaultPolicy` is `true` for a user's organization, and the + // user has never changed their local autotype setting (`autotypeEnabledState`), + // we set their local setting to `true` (once the local user setting is changed + // by this policy or the user themselves, the default policy should + // never change the user setting again). + combineLatest([ + this.autotypeEnabledState.state$, + this.desktopAutotypePolicy.autotypeDefaultSetting$, + ]) + .pipe( + map(async ([autotypeEnabledState, autotypeDefaultPolicy]) => { + if (autotypeDefaultPolicy === true && autotypeEnabledState === null) { + await this.setAutotypeEnabledState(true); + } + }), + ) + .subscribe(); + + // listen for changes in keyboard shortcut settings + this.autotypeKeyboardShortcut$ + .pipe( + concatMap(async (keyboardShortcut) => { + ipc.autofill.configureAutotype(keyboardShortcut); + }), + ) + .subscribe(); + + // listen for changes in factors that are required for enablement of the feature + combineLatest([ + // if the user has enabled the setting + this.autotypeEnabledUserSetting$, + // if the feature flag is set + this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype), + // if there is an active account with an unlocked vault + this.authService.activeAccountStatus$, + // if the user's account is Premium + this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ), + ]) + .pipe( + concatMap(async ([settingsEnabled, ffEnabled, authStatus, hasPremium]) => { + const enabled = + settingsEnabled && + ffEnabled && + authStatus == AuthenticationStatus.Unlocked && + hasPremium; + + ipc.autofill.toggleAutotype(enabled); + }), + ) + .subscribe(); } async setAutotypeEnabledState(enabled: boolean): Promise { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index fbb83a1bf56..2a4c672d7b8 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -310,19 +310,6 @@ export class Main { this.logService, this.windowMain, ); - - app - .whenReady() - .then(() => { - this.mainDesktopAutotypeService.init(); - }) - .catch((reason) => { - this.logService.error("Error initializing Autotype.", reason); - }); - - app.on("will-quit", () => { - this.mainDesktopAutotypeService.disableAutotype(); - }); } bootstrap() {