1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +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

@@ -1,11 +0,0 @@
export class WindowState {
width?: number;
height?: number;
isMaximized?: boolean;
// 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.
displayBounds: any;
x?: number;
y?: number;
zoomFactor?: number;
}

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

View File

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

View File

@@ -1,26 +1,17 @@
import { WindowState } from "../../../models/domain/window-state";
import { ThemeType } from "../../enums";
export class GlobalState {
enableAlwaysOnTop?: boolean;
installedVersion?: string;
locale?: string;
organizationInvitation?: any;
rememberedEmail?: string;
theme?: ThemeType = ThemeType.System;
window?: WindowState = new WindowState();
twoFactorToken?: string;
biometricFingerprintValidated?: boolean;
vaultTimeout?: number;
vaultTimeoutAction?: string;
loginRedirect?: any;
mainWindowSize?: number;
enableTray?: boolean;
enableMinimizeToTray?: boolean;
enableCloseToTray?: boolean;
enableStartToTray?: boolean;
openAtLogin?: boolean;
alwaysShowDock?: boolean;
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: 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 { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key";
import { WindowState } from "../../models/domain/window-state";
import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
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> {
return (
(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> {
return (
(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> {
return (
(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)
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
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> {
return (
(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> {
return (
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> {
const account = await this.getAccount(
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 { MergeEnvironmentState } from "./migrations/45-merge-environment-state";
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 { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
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";
export const MIN_VERSION = 3;
export const CURRENT_VERSION = 46;
export const CURRENT_VERSION = 47;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -98,7 +99,8 @@ export function createMigrationBuilder() {
.with(AutoConfirmFingerPrintsMigrator, 42, 43)
.with(UserDecryptionOptionsMigrator, 43, 44)
.with(MergeEnvironmentState, 44, 45)
.with(DeleteBiometricPromptCancelledData, 45, CURRENT_VERSION);
.with(DeleteBiometricPromptCancelledData, 45, 46)
.with(MoveDesktopSettingsMigrator, 46, CURRENT_VERSION);
}
export async function currentVersion(

View File

@@ -178,12 +178,9 @@ export function mockMigrationHelper(
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[]> = {
/**
* 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;
/**
@@ -282,10 +279,9 @@ function expectInjectedData(
* @param initalData The data to start with
* @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<
TMigrator extends Migrator<number, number>,
TUsers extends readonly string[] = string[],
const TUsers extends readonly string[],
>(
migrator: TMigrator,
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;
}
}