1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-29 22:53:44 +00:00

Desktop Autotype toggle on vault lock/unlock

This commit is contained in:
neuronull
2025-10-24 12:45:14 -06:00
parent e3f943364f
commit 2dd6cd1b3a
4 changed files with 161 additions and 104 deletions

View File

@@ -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[]) {

View File

@@ -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: (

View File

@@ -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<boolean> = of(false);
resolvedAutotypeEnabled$: Observable<boolean> = of(false);
// The keyboard shortcut from the user settings menu
autotypeKeyboardShortcut$: Observable<string[]> = 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<void> {

View File

@@ -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() {