1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-5540] DesktopSettingsService (#8369)

* WIP: First Try at making DesktopSettingsService

Does not work, migrations are ran in renderer but the values are read in main.

* Update window$ retrieval

* Fix DesktopSettings

* Rename Migration

* Add Migration to Builder

* Cleanup

* Update Comments

* Update `migrate.ts`

* Catch Unawaited Promises

* Remove Comments

* Update Tests

* Rename Migration

* Add `alwaysOnTop`

* Make `init` async

* Fix Desktop Build
This commit is contained in:
Justin Baur
2024-03-21 12:53:12 -05:00
committed by GitHub
parent b9f9ad029f
commit b450b31ec4
15 changed files with 459 additions and 263 deletions

View File

@@ -120,8 +120,8 @@ export class SettingsComponent implements OnInit {
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
private dialogService: DialogService, private dialogService: DialogService,
private userVerificationService: UserVerificationServiceAbstraction, private userVerificationService: UserVerificationServiceAbstraction,
private biometricStateService: BiometricStateService,
private desktopSettingsService: DesktopSettingsService, private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService,
) { ) {
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
@@ -253,12 +253,12 @@ export class SettingsComponent implements OnInit {
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$), clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(), minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
enableTray: await this.stateService.getEnableTray(), enableTray: await firstValueFrom(this.desktopSettingsService.trayEnabled$),
enableMinToTray: await this.stateService.getEnableMinimizeToTray(), enableMinToTray: await firstValueFrom(this.desktopSettingsService.minimizeToTray$),
enableCloseToTray: await this.stateService.getEnableCloseToTray(), enableCloseToTray: await firstValueFrom(this.desktopSettingsService.closeToTray$),
startToTray: await this.stateService.getEnableStartToTray(), startToTray: await firstValueFrom(this.desktopSettingsService.startToTray$),
openAtLogin: await this.stateService.getOpenAtLogin(), openAtLogin: await firstValueFrom(this.desktopSettingsService.openAtLogin$),
alwaysShowDock: await this.stateService.getAlwaysShowDock(), alwaysShowDock: await firstValueFrom(this.desktopSettingsService.alwaysShowDock$),
enableBrowserIntegration: await this.stateService.getEnableBrowserIntegration(), enableBrowserIntegration: await this.stateService.getEnableBrowserIntegration(),
enableBrowserIntegrationFingerprint: enableBrowserIntegrationFingerprint:
await this.stateService.getEnableBrowserIntegrationFingerprint(), await this.stateService.getEnableBrowserIntegrationFingerprint(),
@@ -507,16 +507,16 @@ export class SettingsComponent implements OnInit {
} }
async saveMinToTray() { async saveMinToTray() {
await this.stateService.setEnableMinimizeToTray(this.form.value.enableMinToTray); await this.desktopSettingsService.setMinimizeToTray(this.form.value.enableMinToTray);
} }
async saveCloseToTray() { async saveCloseToTray() {
if (this.requireEnableTray) { if (this.requireEnableTray) {
this.form.controls.enableTray.setValue(true); this.form.controls.enableTray.setValue(true);
await this.stateService.setEnableTray(this.form.value.enableTray); await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray);
} }
await this.stateService.setEnableCloseToTray(this.form.value.enableCloseToTray); await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray);
} }
async saveTray() { async saveTray() {
@@ -533,9 +533,9 @@ export class SettingsComponent implements OnInit {
if (confirm) { if (confirm) {
this.form.controls.startToTray.setValue(false, { emitEvent: false }); this.form.controls.startToTray.setValue(false, { emitEvent: false });
await this.stateService.setEnableStartToTray(this.form.value.startToTray); await this.desktopSettingsService.setStartToTray(this.form.value.startToTray);
this.form.controls.enableCloseToTray.setValue(false, { emitEvent: false }); this.form.controls.enableCloseToTray.setValue(false, { emitEvent: false });
await this.stateService.setEnableCloseToTray(this.form.value.enableCloseToTray); await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray);
} else { } else {
this.form.controls.enableTray.setValue(true); this.form.controls.enableTray.setValue(true);
} }
@@ -543,17 +543,18 @@ export class SettingsComponent implements OnInit {
return; return;
} }
await this.stateService.setEnableTray(this.form.value.enableTray); await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray);
// TODO: Ideally the DesktopSettingsService.trayEnabled$ could be subscribed to instead of using messaging.
this.messagingService.send(this.form.value.enableTray ? "showTray" : "removeTray"); this.messagingService.send(this.form.value.enableTray ? "showTray" : "removeTray");
} }
async saveStartToTray() { async saveStartToTray() {
if (this.requireEnableTray) { if (this.requireEnableTray) {
this.form.controls.enableTray.setValue(true); this.form.controls.enableTray.setValue(true);
await this.stateService.setEnableTray(this.form.value.enableTray); await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray);
} }
await this.stateService.setEnableStartToTray(this.form.value.startToTray); await this.desktopSettingsService.setStartToTray(this.form.value.startToTray);
} }
async saveLocale() { async saveLocale() {
@@ -573,13 +574,12 @@ export class SettingsComponent implements OnInit {
} }
async saveAlwaysShowDock() { async saveAlwaysShowDock() {
await this.stateService.setAlwaysShowDock(this.form.value.alwaysShowDock); await this.desktopSettingsService.setAlwaysShowDock(this.form.value.alwaysShowDock);
} }
async saveOpenAtLogin() { async saveOpenAtLogin() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.desktopSettingsService.setOpenAtLogin(this.form.value.openAtLogin);
// eslint-disable-next-line @typescript-eslint/no-floating-promises // TODO: Ideally DesktopSettingsService.openAtLogin$ could be subscribed to directly rather than sending a message
this.stateService.setOpenAtLogin(this.form.value.openAtLogin);
this.messagingService.send( this.messagingService.send(
this.form.value.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin", this.form.value.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin",
); );

View File

@@ -57,8 +57,8 @@ export class Main {
environmentService: DefaultEnvironmentService; environmentService: DefaultEnvironmentService;
mainCryptoFunctionService: MainCryptoFunctionService; mainCryptoFunctionService: MainCryptoFunctionService;
desktopCredentialStorageListener: DesktopCredentialStorageListener; desktopCredentialStorageListener: DesktopCredentialStorageListener;
migrationRunner: MigrationRunner;
desktopSettingsService: DesktopSettingsService; desktopSettingsService: DesktopSettingsService;
migrationRunner: MigrationRunner;
tokenService: TokenServiceAbstraction; tokenService: TokenServiceAbstraction;
windowMain: WindowMain; windowMain: WindowMain;
@@ -179,6 +179,8 @@ export class Main {
false, // Do not use disk caching because this will get out of sync with the renderer service false, // Do not use disk caching because this will get out of sync with the renderer service
); );
this.desktopSettingsService = new DesktopSettingsService(stateProvider);
const biometricStateService = new DefaultBiometricStateService(stateProvider); const biometricStateService = new DefaultBiometricStateService(stateProvider);
this.windowMain = new WindowMain( this.windowMain = new WindowMain(
@@ -186,13 +188,13 @@ export class Main {
biometricStateService, biometricStateService,
this.logService, this.logService,
this.storageService, this.storageService,
this.desktopSettingsService,
(arg) => this.processDeepLink(arg), (arg) => this.processDeepLink(arg),
(win) => this.trayMain.setupWindowListeners(win), (win) => this.trayMain.setupWindowListeners(win),
); );
this.messagingMain = new MessagingMain(this, this.stateService); this.messagingMain = new MessagingMain(this, this.stateService, this.desktopSettingsService);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
this.desktopSettingsService = new DesktopSettingsService(stateProvider);
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
this.messagingMain.onMessage(message); this.messagingMain.onMessage(message);
@@ -244,7 +246,7 @@ export class Main {
await this.toggleHardwareAcceleration(); await this.toggleHardwareAcceleration();
await this.windowMain.init(); await this.windowMain.init();
await this.i18nService.init(); await this.i18nService.init();
this.messagingMain.init(); await this.messagingMain.init();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.menuMain.init(); this.menuMain.init();
@@ -256,10 +258,8 @@ export class Main {
click: () => this.messagingService.send("lockVault"), click: () => this.messagingService.send("lockVault"),
}, },
]); ]);
if (await this.stateService.getEnableStartToTray()) { if (await firstValueFrom(this.desktopSettingsService.startToTray$)) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.trayMain.hideToTray();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.trayMain.hideToTray();
} }
this.powerMonitorMain.init(); this.powerMonitorMain.init();
await this.updaterMain.init(); await this.updaterMain.init();

View File

@@ -6,6 +6,7 @@ import { app, ipcMain } from "electron";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Main } from "../main"; import { Main } from "../main";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { MenuUpdateRequest } from "./menu/menu.updater"; import { MenuUpdateRequest } from "./menu/menu.updater";
@@ -17,19 +18,16 @@ export class MessagingMain {
constructor( constructor(
private main: Main, private main: Main,
private stateService: StateService, private stateService: StateService,
private desktopSettingsService: DesktopSettingsService,
) {} ) {}
init() { async init() {
this.scheduleNextSync(); this.scheduleNextSync();
if (process.platform === "linux") { if (process.platform === "linux") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.desktopSettingsService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile()));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile()));
} else { } else {
const loginSettings = app.getLoginItemSettings(); const loginSettings = app.getLoginItemSettings();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.desktopSettingsService.setOpenAtLogin(loginSettings.openAtLogin);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setOpenAtLogin(loginSettings.openAtLogin);
} }
ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message)); ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message));
} }

View File

@@ -1,9 +1,11 @@
import * as path from "path"; import * as path from "path";
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron"; import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
import { firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { WindowMain } from "./window.main"; import { WindowMain } from "./window.main";
@@ -18,7 +20,7 @@ export class TrayMain {
constructor( constructor(
private windowMain: WindowMain, private windowMain: WindowMain,
private i18nService: I18nService, private i18nService: I18nService,
private stateService: StateService, private desktopSettingsService: DesktopSettingsService,
) { ) {
if (process.platform === "win32") { if (process.platform === "win32") {
this.icon = path.join(__dirname, "/images/icon.ico"); this.icon = path.join(__dirname, "/images/icon.ico");
@@ -54,14 +56,14 @@ export class TrayMain {
} }
this.contextMenu = Menu.buildFromTemplate(menuItemOptions); this.contextMenu = Menu.buildFromTemplate(menuItemOptions);
if (await this.stateService.getEnableTray()) { if (await firstValueFrom(this.desktopSettingsService.trayEnabled$)) {
this.showTray(); this.showTray();
} }
} }
setupWindowListeners(win: BrowserWindow) { setupWindowListeners(win: BrowserWindow) {
win.on("minimize", async (e: Event) => { win.on("minimize", async (e: Event) => {
if (await this.stateService.getEnableMinimizeToTray()) { if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) {
e.preventDefault(); e.preventDefault();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -70,7 +72,7 @@ export class TrayMain {
}); });
win.on("close", async (e: Event) => { win.on("close", async (e: Event) => {
if (await this.stateService.getEnableCloseToTray()) { if (await firstValueFrom(this.desktopSettingsService.closeToTray$)) {
if (!this.windowMain.isQuitting) { if (!this.windowMain.isQuitting) {
e.preventDefault(); e.preventDefault();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -81,7 +83,7 @@ export class TrayMain {
}); });
win.on("show", async () => { win.on("show", async () => {
const enableTray = await this.stateService.getEnableTray(); const enableTray = await firstValueFrom(this.desktopSettingsService.trayEnabled$);
if (!enableTray) { if (!enableTray) {
setTimeout(() => this.removeTray(false), 100); setTimeout(() => this.removeTray(false), 100);
} }
@@ -106,7 +108,7 @@ export class TrayMain {
if (this.windowMain.win != null) { if (this.windowMain.win != null) {
this.windowMain.win.hide(); this.windowMain.win.hide();
} }
if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) { if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) {
this.hideDock(); this.hideDock();
} }
} }
@@ -176,7 +178,7 @@ export class TrayMain {
} }
if (this.windowMain.win.isVisible()) { if (this.windowMain.win.isVisible()) {
this.windowMain.win.hide(); this.windowMain.win.hide();
if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) { if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) {
this.hideDock(); this.hideDock();
} }
} else { } else {

View File

@@ -3,13 +3,15 @@ import * as path from "path";
import * as url from "url"; import * as url from "url";
import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron"; import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron";
import { firstValueFrom } from "rxjs";
import { WindowState } from "@bitwarden/common/models/domain/window-state";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { WindowState } from "../platform/models/domain/window-state";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { import {
cleanUserAgent, cleanUserAgent,
isDev, isDev,
@@ -40,6 +42,7 @@ export class WindowMain {
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private logService: LogService, private logService: LogService,
private storageService: AbstractStorageService, private storageService: AbstractStorageService,
private desktopSettingsService: DesktopSettingsService,
private argvCallback: (argv: string[]) => void = null, private argvCallback: (argv: string[]) => void = null,
private createWindowCallback: (win: BrowserWindow) => void, private createWindowCallback: (win: BrowserWindow) => void,
) {} ) {}
@@ -121,7 +124,7 @@ export class WindowMain {
app.on("activate", async () => { app.on("activate", async () => {
// On OS X it's common to re-create a window in the app when the // On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (this.win === null) { if (this.win == null) {
await this.createWindow(); await this.createWindow();
} else { } else {
// Show the window when clicking on Dock icon // Show the window when clicking on Dock icon
@@ -141,7 +144,7 @@ export class WindowMain {
this.defaultWidth, this.defaultWidth,
this.defaultHeight, this.defaultHeight,
); );
this.enableAlwaysOnTop = await this.stateService.getEnableAlwaysOnTop(); this.enableAlwaysOnTop = await firstValueFrom(this.desktopSettingsService.alwaysOnTop$);
this.session = session.fromPartition("persist:bitwarden", { cache: false }); this.session = session.fromPartition("persist:bitwarden", { cache: false });
@@ -265,7 +268,7 @@ export class WindowMain {
async toggleAlwaysOnTop() { async toggleAlwaysOnTop() {
this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); this.enableAlwaysOnTop = !this.win.isAlwaysOnTop();
this.win.setAlwaysOnTop(this.enableAlwaysOnTop); this.win.setAlwaysOnTop(this.enableAlwaysOnTop);
await this.stateService.setEnableAlwaysOnTop(this.enableAlwaysOnTop); await this.desktopSettingsService.setAlwaysOnTop(this.enableAlwaysOnTop);
} }
private windowStateChangeHandler(configKey: string, win: BrowserWindow) { private windowStateChangeHandler(configKey: string, win: BrowserWindow) {
@@ -284,7 +287,7 @@ export class WindowMain {
const bounds = win.getBounds(); const bounds = win.getBounds();
if (this.windowStates[configKey] == null) { if (this.windowStates[configKey] == null) {
this.windowStates[configKey] = await this.stateService.getWindow(); this.windowStates[configKey] = await firstValueFrom(this.desktopSettingsService.window$);
if (this.windowStates[configKey] == null) { if (this.windowStates[configKey] == null) {
this.windowStates[configKey] = <WindowState>{}; this.windowStates[configKey] = <WindowState>{};
} }
@@ -304,14 +307,14 @@ export class WindowMain {
this.windowStates[configKey].zoomFactor = win.webContents.zoomFactor; this.windowStates[configKey].zoomFactor = win.webContents.zoomFactor;
} }
await this.stateService.setWindow(this.windowStates[configKey]); await this.desktopSettingsService.setWindow(this.windowStates[configKey]);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
} }
private async getWindowState(defaultWidth: number, defaultHeight: number) { private async getWindowState(defaultWidth: number, defaultHeight: number) {
const state = await this.stateService.getWindow(); const state = await firstValueFrom(this.desktopSettingsService.window$);
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
let displayBounds: Electron.Rectangle = null; let displayBounds: Electron.Rectangle = null;

View File

@@ -4,7 +4,7 @@ export class WindowState {
isMaximized?: boolean; isMaximized?: boolean;
// TODO: displayBounds is an Electron.Rectangle. // TODO: displayBounds is an Electron.Rectangle.
// We need to establish some kind of client-specific global state, similar to the way we already extend a base Account. // We need to establish some kind of client-specific global state, similar to the way we already extend a base Account.
displayBounds: any; displayBounds: Electron.Rectangle;
x?: number; x?: number;
y?: number; y?: number;
zoomFactor?: number; zoomFactor?: number;

View File

@@ -1,4 +1,4 @@
import { map } from "rxjs"; import { Observable, map } from "rxjs";
import { import {
DESKTOP_SETTINGS_DISK, DESKTOP_SETTINGS_DISK,
@@ -6,6 +6,8 @@ import {
StateProvider, StateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
import { WindowState } from "../models/domain/window-state";
export const HARDWARE_ACCELERATION = new KeyDefinition<boolean>( export const HARDWARE_ACCELERATION = new KeyDefinition<boolean>(
DESKTOP_SETTINGS_DISK, DESKTOP_SETTINGS_DISK,
"hardwareAcceleration", "hardwareAcceleration",
@@ -14,13 +16,165 @@ export const HARDWARE_ACCELERATION = new KeyDefinition<boolean>(
}, },
); );
const WINDOW_KEY = new KeyDefinition<WindowState | null>(DESKTOP_SETTINGS_DISK, "window", {
deserializer: (s) => s,
});
const CLOSE_TO_TRAY_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "closeToTray", {
deserializer: (b) => b,
});
const MINIMIZE_TO_TRAY_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "minimizeToTray", {
deserializer: (b) => b,
});
const START_TO_TRAY_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "startToTray", {
deserializer: (b) => b,
});
const TRAY_ENABLED_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "trayEnabled", {
deserializer: (b) => b,
});
const OPEN_AT_LOGIN_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "openAtLogin", {
deserializer: (b) => b,
});
const ALWAYS_SHOW_DOCK_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "alwaysShowDock", {
deserializer: (b) => b,
});
const ALWAYS_ON_TOP_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "alwaysOnTop", {
deserializer: (b) => b,
});
/**
* Various settings for controlling application behavior specific to the desktop client.
*/
export class DesktopSettingsService { export class DesktopSettingsService {
private hwState = this.stateProvider.getGlobal(HARDWARE_ACCELERATION); private hwState = this.stateProvider.getGlobal(HARDWARE_ACCELERATION);
hardwareAcceleration$ = this.hwState.state$.pipe(map((v) => v ?? true)); hardwareAcceleration$ = this.hwState.state$.pipe(map((v) => v ?? true));
constructor(private stateProvider: StateProvider) {} private readonly windowState = this.stateProvider.getGlobal(WINDOW_KEY);
private readonly closeToTrayState = this.stateProvider.getGlobal(CLOSE_TO_TRAY_KEY);
/**
* Tha applications setting for whether or not to close the application into the system tray.
*/
closeToTray$ = this.closeToTrayState.state$.pipe(map((value) => value ?? false));
private readonly minimizeToTrayState = this.stateProvider.getGlobal(MINIMIZE_TO_TRAY_KEY);
/**
* The application setting for whether or not to minimize the applicaiton into the system tray.
*/
minimizeToTray$ = this.minimizeToTrayState.state$.pipe(map((value) => value ?? false));
private readonly startToTrayState = this.stateProvider.getGlobal(START_TO_TRAY_KEY);
/**
* The application setting for whether or not to start the application into the system tray.
*/
startToTray$ = this.startToTrayState.state$.pipe(map((value) => value ?? false));
private readonly trayEnabledState = this.stateProvider.getGlobal(TRAY_ENABLED_KEY);
/**
* Whether or not the system tray has been enabled.
*/
trayEnabled$ = this.trayEnabledState.state$.pipe(map((value) => value ?? false));
private readonly openAtLoginState = this.stateProvider.getGlobal(OPEN_AT_LOGIN_KEY);
/**
* The application setting for whether or not the application should open at system login.
*/
openAtLogin$ = this.openAtLoginState.state$.pipe(map((value) => value ?? false));
private readonly alwaysShowDockState = this.stateProvider.getGlobal(ALWAYS_SHOW_DOCK_KEY);
/**
* The application setting for whether or not the application should show up in the dock.
*/
alwaysShowDock$ = this.alwaysShowDockState.state$.pipe(map((value) => value ?? false));
private readonly alwaysOnTopState = this.stateProvider.getGlobal(ALWAYS_ON_TOP_KEY);
alwaysOnTop$ = this.alwaysOnTopState.state$.pipe(map((value) => value ?? false));
constructor(private stateProvider: StateProvider) {
this.window$ = this.windowState.state$.pipe(
map((window) =>
window != null && Object.keys(window).length > 0 ? window : new WindowState(),
),
);
}
async setHardwareAcceleration(enabled: boolean) { async setHardwareAcceleration(enabled: boolean) {
await this.hwState.update(() => enabled); await this.hwState.update(() => enabled);
} }
/**
* The applications current window state.
*/
window$: Observable<WindowState>;
/**
* Updates the window state of the application so that the application can reopen in the same place as it was closed from.
* @param windowState The window state to set.
*/
async setWindow(windowState: WindowState) {
await this.windowState.update(() => windowState);
}
/**
* Sets the setting for whether or not the application should go into the system tray when closed.
* @param value `true` if the application should go into the system tray when closed, `false` if it should not.
*/
async setCloseToTray(value: boolean) {
await this.closeToTrayState.update(() => value);
}
/**
* Sets the setting for whether or not the application should go into the tray when minimized.
* @param value `true` if the application should minimize into the system tray, `false` if it should not.
*/
async setMinimizeToTray(value: boolean) {
await this.minimizeToTrayState.update(() => value);
}
/**
* Sets the setting for whether or not the application should be started into the system tray.
* @param value `true` if the application should be started to the tray`, `false` if it should not.
*/
async setStartToTray(value: boolean) {
await this.startToTrayState.update(() => value);
}
/**
* Sets the setting for whether or not the application be shown in the system tray.
* @param value `true` if the application should show in the tray, `false` if it should not.
*/
async setTrayEnabled(value: boolean) {
await this.trayEnabledState.update(() => value);
}
/**
* Sets the setting for whether or not the application should open at login of the computer.
* @param value `true` if the application should open at login, `false` if it should not.
*/
async setOpenAtLogin(value: boolean) {
await this.openAtLoginState.update(() => value);
}
/**
* Sets the setting for whether or not the application should be shown in the dock.
* @param value `true` if the application should should in the dock, `false` if it should not.
*/
async setAlwaysShowDock(value: boolean) {
await this.alwaysShowDockState.update(() => value);
}
/**
* Sets the setting for whether or not the application should stay on top of all other windows.
* @param value `true` if the application should stay on top, `false` if it should not.
*/
async setAlwaysOnTop(value: boolean) {
await this.alwaysOnTopState.update(() => value);
}
} }

View File

@@ -4,7 +4,6 @@ import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-re
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key"; import { BiometricKey } from "../../auth/types/biometric-key";
import { WindowState } from "../../models/domain/window-state";
import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
@@ -52,8 +51,6 @@ export abstract class StateService<T extends Account = Account> {
getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>; getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>;
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
@@ -184,8 +181,6 @@ export abstract class StateService<T extends Account = Account> {
setEmail: (value: string, options?: StorageOptions) => Promise<void>; setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>; getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>; setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableAlwaysOnTop: (options?: StorageOptions) => Promise<boolean>;
setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableBrowserIntegration: (options?: StorageOptions) => Promise<boolean>; getEnableBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise<void>; setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise<boolean>; getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise<boolean>;
@@ -193,19 +188,11 @@ export abstract class StateService<T extends Account = Account> {
value: boolean, value: boolean,
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => Promise<void>;
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise<boolean>; getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
setEnableDuckDuckGoBrowserIntegration: ( setEnableDuckDuckGoBrowserIntegration: (
value: boolean, value: boolean,
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => Promise<void>;
getEnableMinimizeToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableStartToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableTray: (options?: StorageOptions) => Promise<boolean>;
setEnableTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>;
setEncryptedCiphers: ( setEncryptedCiphers: (
value: { [id: string]: CipherData }, value: { [id: string]: CipherData },
@@ -261,12 +248,8 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>; ) => Promise<void>;
getLocale: (options?: StorageOptions) => Promise<string>; getLocale: (options?: StorageOptions) => Promise<string>;
setLocale: (value: string, options?: StorageOptions) => Promise<void>; setLocale: (value: string, options?: StorageOptions) => Promise<void>;
getMainWindowSize: (options?: StorageOptions) => Promise<number>;
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>; setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<PasswordGeneratorOptions>; getPasswordGenerationOptions: (options?: StorageOptions) => Promise<PasswordGeneratorOptions>;
@@ -302,8 +285,6 @@ export abstract class StateService<T extends Account = Account> {
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>; getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>;
setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>; setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>;
getWindow: () => Promise<WindowState>;
setWindow: (value: WindowState) => Promise<void>;
/** /**
* @deprecated Do not call this directly, use ConfigService * @deprecated Do not call this directly, use ConfigService
*/ */

View File

@@ -186,7 +186,6 @@ export class AccountProfile {
export class AccountSettings { export class AccountSettings {
defaultUriMatch?: UriMatchStrategySetting; defaultUriMatch?: UriMatchStrategySetting;
disableGa?: boolean; disableGa?: boolean;
enableAlwaysOnTop?: boolean;
enableBiometric?: boolean; enableBiometric?: boolean;
minimizeOnCopyToClipboard?: boolean; minimizeOnCopyToClipboard?: boolean;
passwordGenerationOptions?: PasswordGeneratorOptions; passwordGenerationOptions?: PasswordGeneratorOptions;

View File

@@ -1,26 +1,17 @@
import { WindowState } from "../../../models/domain/window-state";
import { ThemeType } from "../../enums"; import { ThemeType } from "../../enums";
export class GlobalState { export class GlobalState {
enableAlwaysOnTop?: boolean;
installedVersion?: string; installedVersion?: string;
locale?: string; locale?: string;
organizationInvitation?: any; organizationInvitation?: any;
rememberedEmail?: string; rememberedEmail?: string;
theme?: ThemeType = ThemeType.System; theme?: ThemeType = ThemeType.System;
window?: WindowState = new WindowState();
twoFactorToken?: string; twoFactorToken?: string;
biometricFingerprintValidated?: boolean; biometricFingerprintValidated?: boolean;
vaultTimeout?: number; vaultTimeout?: number;
vaultTimeoutAction?: string; vaultTimeoutAction?: string;
loginRedirect?: any; loginRedirect?: any;
mainWindowSize?: number; mainWindowSize?: number;
enableTray?: boolean;
enableMinimizeToTray?: boolean;
enableCloseToTray?: boolean;
enableStartToTray?: boolean;
openAtLogin?: boolean;
alwaysShowDock?: boolean;
enableBrowserIntegration?: boolean; enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean; enableDuckDuckGoBrowserIntegration?: boolean;

View File

@@ -8,7 +8,6 @@ import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-re
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key"; import { BiometricKey } from "../../auth/types/biometric-key";
import { WindowState } from "../../models/domain/window-state";
import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
@@ -277,24 +276,6 @@ export class StateService<
); );
} }
async getAlwaysShowDock(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.alwaysShowDock ?? false
);
}
async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.alwaysShowDock = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getBiometricFingerprintValidated(options?: StorageOptions): Promise<boolean> { async getBiometricFingerprintValidated(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -847,36 +828,6 @@ export class StateService<
); );
} }
async getEnableAlwaysOnTop(options?: StorageOptions): Promise<boolean> {
const accountPreference = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.enableAlwaysOnTop;
const globalPreference = (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.enableAlwaysOnTop;
return accountPreference ?? globalPreference ?? false;
}
async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.enableAlwaysOnTop = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.enableAlwaysOnTop = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getEnableBrowserIntegration(options?: StorageOptions): Promise<boolean> { async getEnableBrowserIntegration(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -916,24 +867,6 @@ export class StateService<
); );
} }
async getEnableCloseToTray(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableCloseToTray ?? false
);
}
async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.enableCloseToTray = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise<boolean> { async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -955,60 +888,6 @@ export class StateService<
); );
} }
async getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableMinimizeToTray ?? false
);
}
async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.enableMinimizeToTray = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getEnableStartToTray(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableStartToTray ?? false
);
}
async setEnableStartToTray(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.enableStartToTray = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getEnableTray(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableTray ?? false
);
}
async setEnableTray(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.enableTray = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
@withPrototypeForObjectValues(CipherData) @withPrototypeForObjectValues(CipherData)
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
return ( return (
@@ -1309,23 +1188,6 @@ export class StateService<
); );
} }
async getMainWindowSize(options?: StorageOptions): Promise<number> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.mainWindowSize;
}
async setMainWindowSize(value: number, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
globals.mainWindowSize = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise<boolean> { async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -1344,24 +1206,6 @@ export class StateService<
); );
} }
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.openAtLogin ?? false
);
}
async setOpenAtLogin(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.openAtLogin = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOrganizationInvitation(options?: StorageOptions): Promise<any> { async getOrganizationInvitation(options?: StorageOptions): Promise<any> {
return ( return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions())) await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@@ -1571,24 +1415,6 @@ export class StateService<
); );
} }
async getWindow(): Promise<WindowState> {
const globals = await this.getGlobals(await this.defaultOnDiskOptions());
return globals?.window != null && Object.keys(globals.window).length > 0
? globals.window
: new WindowState();
}
async setWindow(value: WindowState, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.window = value;
return await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise<void> { async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise<void> {
const account = await this.getAccount( const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),

View File

@@ -42,6 +42,7 @@ import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confi
import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider"; import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider";
import { MergeEnvironmentState } from "./migrations/45-merge-environment-state"; import { MergeEnvironmentState } from "./migrations/45-merge-environment-state";
import { DeleteBiometricPromptCancelledData } from "./migrations/46-delete-orphaned-biometric-prompt-data"; import { DeleteBiometricPromptCancelledData } from "./migrations/46-delete-orphaned-biometric-prompt-data";
import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settings";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
@@ -50,7 +51,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 46; export const CURRENT_VERSION = 47;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@@ -98,7 +99,8 @@ export function createMigrationBuilder() {
.with(AutoConfirmFingerPrintsMigrator, 42, 43) .with(AutoConfirmFingerPrintsMigrator, 42, 43)
.with(UserDecryptionOptionsMigrator, 43, 44) .with(UserDecryptionOptionsMigrator, 43, 44)
.with(MergeEnvironmentState, 44, 45) .with(MergeEnvironmentState, 44, 45)
.with(DeleteBiometricPromptCancelledData, 45, CURRENT_VERSION); .with(DeleteBiometricPromptCancelledData, 45, 46)
.with(MoveDesktopSettingsMigrator, 46, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@@ -178,12 +178,9 @@ export function mockMigrationHelper(
return mockHelper; return mockHelper;
} }
// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves
export type InitialDataHint<TUsers extends readonly string[]> = { export type InitialDataHint<TUsers extends readonly string[]> = {
/** /**
* A string array of the users id who are authenticated * A string array of the users id who are authenticated
*
* NOTE: It's recommended to as const this string array so you get type help defining the users data
*/ */
authenticatedAccounts?: TUsers; authenticatedAccounts?: TUsers;
/** /**
@@ -282,10 +279,9 @@ function expectInjectedData(
* @param initalData The data to start with * @param initalData The data to start with
* @returns State after your migration has ran. * @returns State after your migration has ran.
*/ */
// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves
export async function runMigrator< export async function runMigrator<
TMigrator extends Migrator<number, number>, TMigrator extends Migrator<number, number>,
TUsers extends readonly string[] = string[], const TUsers extends readonly string[],
>( >(
migrator: TMigrator, migrator: TMigrator,
initalData?: InitialDataHint<TUsers>, initalData?: InitialDataHint<TUsers>,

View File

@@ -0,0 +1,116 @@
import { runMigrator } from "../migration-helper.spec";
import { MoveDesktopSettingsMigrator } from "./47-move-desktop-settings";
describe("MoveDesktopSettings", () => {
const sut = new MoveDesktopSettingsMigrator(46, 47);
it("can migrate truthy values", async () => {
const output = await runMigrator(sut, {
authenticatedAccounts: ["user1"],
global: {
window: {
width: 400,
height: 400,
displayBounds: {
height: 200,
width: 200,
x: 200,
y: 200,
},
},
enableAlwaysOnTop: true,
enableCloseToTray: true,
enableMinimizeToTray: true,
enableStartToTray: true,
enableTray: true,
openAtLogin: true,
alwaysShowDock: true,
},
user1: {
settings: {
enableAlwaysOnTop: true,
},
},
});
expect(output).toEqual({
authenticatedAccounts: ["user1"],
global: {},
global_desktopSettings_window: {
width: 400,
height: 400,
displayBounds: {
height: 200,
width: 200,
x: 200,
y: 200,
},
},
global_desktopSettings_closeToTray: true,
global_desktopSettings_minimizeToTray: true,
global_desktopSettings_startToTray: true,
global_desktopSettings_trayEnabled: true,
global_desktopSettings_openAtLogin: true,
global_desktopSettings_alwaysShowDock: true,
global_desktopSettings_alwaysOnTop: true,
user1: {
settings: {},
},
});
});
it("can migrate falsey values", async () => {
const output = await runMigrator(sut, {
authenticatedAccounts: ["user1"],
global: {
window: null,
enableCloseToTray: false,
enableMinimizeToTray: false,
enableStartToTray: false,
enableTray: false,
openAtLogin: false,
alwaysShowDock: false,
enableAlwaysOnTop: false,
},
user1: {
settings: {
enableAlwaysOnTop: false,
},
},
});
expect(output).toEqual({
authenticatedAccounts: ["user1"],
global: {},
global_desktopSettings_window: null,
global_desktopSettings_closeToTray: false,
global_desktopSettings_minimizeToTray: false,
global_desktopSettings_startToTray: false,
global_desktopSettings_trayEnabled: false,
global_desktopSettings_openAtLogin: false,
global_desktopSettings_alwaysShowDock: false,
global_desktopSettings_alwaysOnTop: false,
user1: {
settings: {},
},
});
});
it("can migrate even if none of our values are found", async () => {
//
const output = await runMigrator(sut, {
authenticatedAccounts: ["user1"] as const,
global: {
anotherSetting: "",
},
});
expect(output).toEqual({
authenticatedAccounts: ["user1"] as const,
global: {
anotherSetting: "",
},
});
});
});

View File

@@ -0,0 +1,128 @@
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
import { IRREVERSIBLE, Migrator } from "../migrator";
type ExpectedGlobalType = {
window?: object;
enableTray?: boolean;
enableMinimizeToTray?: boolean;
enableCloseToTray?: boolean;
enableStartToTray?: boolean;
openAtLogin?: boolean;
alwaysShowDock?: boolean;
enableAlwaysOnTop?: boolean;
};
type ExpectedAccountType = {
settings?: {
enableAlwaysOnTop?: boolean;
};
};
const DESKTOP_SETTINGS_STATE: StateDefinitionLike = { name: "desktopSettings" };
const WINDOW_KEY: KeyDefinitionLike = { key: "window", stateDefinition: DESKTOP_SETTINGS_STATE };
const CLOSE_TO_TRAY_KEY: KeyDefinitionLike = {
key: "closeToTray",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const MINIMIZE_TO_TRAY_KEY: KeyDefinitionLike = {
key: "minimizeToTray",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const START_TO_TRAY_KEY: KeyDefinitionLike = {
key: "startToTray",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const TRAY_ENABLED_KEY: KeyDefinitionLike = {
key: "trayEnabled",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const OPEN_AT_LOGIN_KEY: KeyDefinitionLike = {
key: "openAtLogin",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const ALWAYS_SHOW_DOCK_KEY: KeyDefinitionLike = {
key: "alwaysShowDock",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const ALWAYS_ON_TOP_KEY: KeyDefinitionLike = {
key: "alwaysOnTop",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
export class MoveDesktopSettingsMigrator extends Migrator<46, 47> {
async migrate(helper: MigrationHelper): Promise<void> {
const legacyGlobal = await helper.get<ExpectedGlobalType>("global");
let updatedGlobal = false;
if (legacyGlobal?.window !== undefined) {
await helper.setToGlobal(WINDOW_KEY, legacyGlobal.window);
updatedGlobal = true;
delete legacyGlobal.window;
}
if (legacyGlobal?.enableCloseToTray != null) {
await helper.setToGlobal(CLOSE_TO_TRAY_KEY, legacyGlobal.enableCloseToTray);
updatedGlobal = true;
delete legacyGlobal.enableCloseToTray;
}
if (legacyGlobal?.enableMinimizeToTray != null) {
await helper.setToGlobal(MINIMIZE_TO_TRAY_KEY, legacyGlobal.enableMinimizeToTray);
updatedGlobal = true;
delete legacyGlobal.enableMinimizeToTray;
}
if (legacyGlobal?.enableStartToTray != null) {
await helper.setToGlobal(START_TO_TRAY_KEY, legacyGlobal.enableStartToTray);
updatedGlobal = true;
delete legacyGlobal.enableStartToTray;
}
if (legacyGlobal?.enableTray != null) {
await helper.setToGlobal(TRAY_ENABLED_KEY, legacyGlobal.enableTray);
updatedGlobal = true;
delete legacyGlobal.enableTray;
}
if (legacyGlobal?.openAtLogin != null) {
await helper.setToGlobal(OPEN_AT_LOGIN_KEY, legacyGlobal.openAtLogin);
updatedGlobal = true;
delete legacyGlobal.openAtLogin;
}
if (legacyGlobal?.alwaysShowDock != null) {
await helper.setToGlobal(ALWAYS_SHOW_DOCK_KEY, legacyGlobal.alwaysShowDock);
updatedGlobal = true;
delete legacyGlobal.alwaysShowDock;
}
if (legacyGlobal?.enableAlwaysOnTop != null) {
await helper.setToGlobal(ALWAYS_ON_TOP_KEY, legacyGlobal.enableAlwaysOnTop);
updatedGlobal = true;
delete legacyGlobal.enableAlwaysOnTop;
}
if (updatedGlobal) {
await helper.set("global", legacyGlobal);
}
async function migrateAccount(userId: string, account: ExpectedAccountType) {
// We only migrate the global setting for this, if we find it on the account object
// just delete it.
if (account?.settings?.enableAlwaysOnTop != null) {
delete account.settings.enableAlwaysOnTop;
await helper.set(userId, account);
}
}
const accounts = await helper.getAccounts<ExpectedAccountType>();
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
}
rollback(helper: MigrationHelper): Promise<void> {
throw IRREVERSIBLE;
}
}