1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-5539] Migrate ThemingService (#8219)

* Update ThemingService

* Finish ThemingService

* Lint

* More Tests & Docs

* Refactor to ThemeStateService

* Rename File

* Fix Import

* Remove `type` added to imports

* Update InitServices

* Fix Test

* Remove Unreferenced Code

* Remove Unneeded Null Check

* Add Ticket Link

* Add Back THEMING_DISK

* Fix Desktop

* Create SYSTEM_THEME_OBSERVABLE

* Fix Browser Injection

* Update Desktop Manual Access

* Fix Default Theme

* Update Test
This commit is contained in:
Justin Baur
2024-03-13 10:25:39 -05:00
committed by GitHub
parent 531ae3184f
commit e6fe0d1d13
38 changed files with 396 additions and 222 deletions

View File

@@ -18,7 +18,7 @@ import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { KdfType, ThemeType } from "../enums";
import { KdfType } from "../enums";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountDecryptionOptions } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
@@ -342,8 +342,6 @@ export abstract class StateService<T extends Account = Account> {
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
getUserId: (options?: StorageOptions) => Promise<string>;

View File

@@ -32,7 +32,7 @@ import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { HtmlStorageLocation, KdfType, StorageLocation, ThemeType } from "../enums";
import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums";
import { StateFactory } from "../factories/state-factory";
import { Utils } from "../misc/utils";
import { ServerConfigData } from "../models/data/server-config.data";
@@ -1754,23 +1754,6 @@ export class StateService<
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.theme;
}
async setTheme(value: ThemeType, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
);
globals.theme = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
);
}
async getTwoFactorToken(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))

View File

@@ -63,6 +63,7 @@ export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
export const THEMING_DISK = new StateDefinition("theming", "disk");
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
// Secrets Manager

View File

@@ -0,0 +1,38 @@
import { Observable, map } from "rxjs";
import { ThemeType } from "../enums";
import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "../state";
export abstract class ThemeStateService {
/**
* The users selected theme.
*/
selectedTheme$: Observable<ThemeType>;
/**
* A method for updating the current users configured theme.
* @param theme The chosen user theme.
*/
setSelectedTheme: (theme: ThemeType) => Promise<void>;
}
const THEME_SELECTION = new KeyDefinition<ThemeType>(THEMING_DISK, "selection", {
deserializer: (s) => s,
});
export class DefaultThemeStateService implements ThemeStateService {
private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
selectedTheme$ = this.selectedThemeState.state$.pipe(map((theme) => theme ?? this.defaultTheme));
constructor(
private globalStateProvider: GlobalStateProvider,
private defaultTheme: ThemeType = ThemeType.System,
) {}
async setSelectedTheme(theme: ThemeType): Promise<void> {
await this.selectedThemeState.update(() => theme, {
shouldUpdate: (currentTheme) => currentTheme !== theme,
});
}
}

View File

@@ -30,6 +30,7 @@ import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-m
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
import { MoveThemeToStateProviderMigrator } from "./migrations/35-move-theme-to-state-providers";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
@@ -39,7 +40,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2;
export const CURRENT_VERSION = 34;
export const CURRENT_VERSION = 35;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -76,7 +77,8 @@ export function createMigrationBuilder() {
.with(EnableContextMenuMigrator, 30, 31)
.with(PreferredLanguageMigrator, 31, 32)
.with(AppIdMigrator, 32, 33)
.with(DomainSettingsMigrator, 33, CURRENT_VERSION);
.with(DomainSettingsMigrator, 33, 34)
.with(MoveThemeToStateProviderMigrator, 34, CURRENT_VERSION);
}
export async function currentVersion(

View File

@@ -286,7 +286,11 @@ function expectInjectedData(
export async function runMigrator<
TMigrator extends Migrator<number, number>,
TUsers extends readonly string[] = string[],
>(migrator: TMigrator, initalData?: InitialDataHint<TUsers>): Promise<Record<string, unknown>> {
>(
migrator: TMigrator,
initalData?: InitialDataHint<TUsers>,
direction: "migrate" | "rollback" = "migrate",
): Promise<Record<string, unknown>> {
// Inject fake data at every level of the object
const allInjectedData = injectData(initalData, []);
@@ -294,7 +298,11 @@ export async function runMigrator<
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
// Run their migrations
await migrator.migrate(helper);
if (direction === "rollback") {
await migrator.rollback(helper);
} else {
await migrator.migrate(helper);
}
const [data, leftoverInjectedData] = expectInjectedData(
fakeStorageService.internalStore,
allInjectedData,

View File

@@ -0,0 +1,78 @@
import { runMigrator } from "../migration-helper.spec";
import { MoveThemeToStateProviderMigrator } from "./35-move-theme-to-state-providers";
describe("MoveThemeToStateProviders", () => {
const sut = new MoveThemeToStateProviderMigrator(34, 35);
describe("migrate", () => {
it("migrates global theme and deletes it", async () => {
const output = await runMigrator(sut, {
global: {
theme: "dark",
},
});
expect(output).toEqual({
global_theming_selection: "dark",
global: {},
});
});
it.each([{}, null])(
"doesn't touch it if global state looks like: '%s'",
async (globalState) => {
const output = await runMigrator(sut, {
global: globalState,
});
expect(output).toEqual({
global: globalState,
});
},
);
});
describe("rollback", () => {
it("migrates state provider theme back to original location when no global", async () => {
const output = await runMigrator(
sut,
{
global_theming_selection: "disk",
},
"rollback",
);
expect(output).toEqual({
global: {
theme: "disk",
},
});
});
it("migrates state provider theme back to legacy location when there is an existing global object", async () => {
const output = await runMigrator(
sut,
{
global_theming_selection: "disk",
global: {
other: "stuff",
},
},
"rollback",
);
expect(output).toEqual({
global: {
theme: "disk",
other: "stuff",
},
});
});
it("does nothing if no theme in state provider location", async () => {
const output = await runMigrator(sut, {}, "rollback");
expect(output).toEqual({});
});
});
});

View File

@@ -0,0 +1,31 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedGlobal = { theme?: string };
const THEME_SELECTION: KeyDefinitionLike = {
key: "selection",
stateDefinition: { name: "theming" },
};
export class MoveThemeToStateProviderMigrator extends Migrator<34, 35> {
async migrate(helper: MigrationHelper): Promise<void> {
const legacyGlobalState = await helper.get<ExpectedGlobal>("global");
const theme = legacyGlobalState?.theme;
if (theme != null) {
await helper.setToGlobal(THEME_SELECTION, theme);
delete legacyGlobalState.theme;
await helper.set("global", legacyGlobalState);
}
}
async rollback(helper: MigrationHelper): Promise<void> {
const theme = await helper.getFromGlobal<string>(THEME_SELECTION);
if (theme != null) {
const legacyGlobal = (await helper.get<ExpectedGlobal>("global")) ?? {};
legacyGlobal.theme = theme;
await helper.set("global", legacyGlobal);
await helper.removeFromGlobal(THEME_SELECTION);
}
}
}