1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-09 20:13:42 +00:00

[PM-26056] Consolidated session timeout component (#16988)

* consolidated session timeout settings component

* rename preferences to appearance

* race condition bug on computed signal

* outdated header for browser

* unnecessary padding

* remove required on action, fix build

* rename localization key

* missing user id

* required

* cleanup task

* eslint fix signals rollback

* takeUntilDestroyed, null checks

* move browser specific logic outside shared component

* explicit input type

* input name

* takeUntilDestroyed, no toast

* unit tests

* cleanup

* cleanup, correct link to deprecation jira

* tech debt todo with jira

* missing web localization key when policy is on

* relative import

* extracting timeout options to component service

* duplicate localization key

* fix failing test

* subsequent timeout action selecting opening without dialog on first dialog cancellation

* default locale can be null

* unit tests failing

* rename, simplifications

* one if else feature flag

* timeout input component rendering before async pipe completion
This commit is contained in:
Maciej Zieniuk
2025-11-11 15:15:36 +01:00
committed by GitHub
parent a66227638e
commit 021d3e53aa
38 changed files with 1660 additions and 80 deletions

View File

@@ -2,7 +2,10 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { SessionTimeoutComponent } from "../../../key-management/session-timeout/session-timeout.component";
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
import { PasswordSettingsComponent } from "./password-settings/password-settings.component";
@@ -15,7 +18,20 @@ const routes: Routes = [
component: SecurityComponent,
data: { titleId: "security" },
children: [
{ path: "", pathMatch: "full", redirectTo: "password" },
{ path: "", pathMatch: "full", redirectTo: "session-timeout" },
{
path: "session-timeout",
component: SessionTimeoutComponent,
canActivate: [
canAccessFeature(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
true,
"/settings/security/password",
false,
),
],
data: { titleId: "sessionTimeoutHeader" },
},
{
path: "password",
component: PasswordSettingsComponent,

View File

@@ -1,8 +1,11 @@
<app-header>
<bit-tab-nav-bar slot="tabs">
<ng-container *ngIf="showChangePassword">
@if (consolidatedSessionTimeoutComponent$ | async) {
<bit-tab-link route="session-timeout">{{ "sessionTimeoutHeader" | i18n }}</bit-tab-link>
}
@if (showChangePassword) {
<bit-tab-link [route]="changePasswordRoute">{{ "masterPassword" | i18n }}</bit-tab-link>
</ng-container>
}
<bit-tab-link route="two-factor">{{ "twoStepLogin" | i18n }}</bit-tab-link>
<bit-tab-link route="device-management">{{ "devices" | i18n }}</bit-tab-link>
<bit-tab-link route="security-keys">{{ "keys" | i18n }}</bit-tab-link>

View File

@@ -1,6 +1,9 @@
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
@@ -14,8 +17,16 @@ import { SharedModule } from "../../../shared";
export class SecurityComponent implements OnInit {
showChangePassword = true;
changePasswordRoute = "password";
consolidatedSessionTimeoutComponent$: Observable<boolean>;
constructor(private userVerificationService: UserVerificationService) {}
constructor(
private userVerificationService: UserVerificationService,
private configService: ConfigService,
) {
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
);
}
async ngOnInit() {
this.showChangePassword = await this.userVerificationService.hasMasterPassword();

View File

@@ -117,10 +117,14 @@ import {
KeyService as KeyServiceAbstraction,
BiometricsService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import {
LockComponentService,
SessionTimeoutSettingsComponentService,
} from "@bitwarden/key-management-ui";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service";
import { WebSessionTimeoutSettingsComponentService } from "@bitwarden/web-vault/app/key-management/session-timeout/services/web-session-timeout-settings-component.service";
import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/vault/services/web-premium-upgrade-prompt.service";
import { flagEnabled } from "../../utils/flags";
@@ -465,6 +469,11 @@ const safeProviders: SafeProvider[] = [
useClass: WebSystemService,
deps: [],
}),
safeProvider({
provide: SessionTimeoutSettingsComponentService,
useClass: WebSessionTimeoutSettingsComponentService,
deps: [I18nServiceAbstraction, PlatformUtilsService],
}),
];
@NgModule({

View File

@@ -0,0 +1,39 @@
import { defer, Observable, of } from "rxjs";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
export class WebSessionTimeoutSettingsComponentService
implements SessionTimeoutSettingsComponentService
{
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() => {
const options: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 },
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
{ name: this.i18nService.t("oneHour"), value: 60 },
{ name: this.i18nService.t("fourHours"), value: 240 },
{ name: this.i18nService.t("onRefresh"), value: VaultTimeoutStringType.OnRestart },
];
if (this.platformUtilsService.isDev()) {
options.push({ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never });
}
return of(options);
});
constructor(
private readonly i18nService: I18nService,
private readonly platformUtilsService: PlatformUtilsService,
) {}
onTimeoutSave(_: VaultTimeout): void {}
}

View File

@@ -0,0 +1,5 @@
<h2 class="tw-mt-6 tw-mb-2 tw-pb-2.5">{{ "sessionTimeoutHeader" | i18n }}</h2>
<div class="tw-max-w-lg">
<bit-session-timeout-settings />
</div>

View File

@@ -0,0 +1,11 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SessionTimeoutSettingsComponent } from "@bitwarden/key-management-ui";
@Component({
templateUrl: "session-timeout.component.html",
imports: [SessionTimeoutSettingsComponent, JslibModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SessionTimeoutComponent {}

View File

@@ -13,7 +13,11 @@
<bit-nav-group icon="bwi-cog" [text]="'settings' | i18n" route="settings">
<bit-nav-item [text]="'myAccount' | i18n" route="settings/account"></bit-nav-item>
<bit-nav-item [text]="'security' | i18n" route="settings/security"></bit-nav-item>
<bit-nav-item [text]="'preferences' | i18n" route="settings/preferences"></bit-nav-item>
@if (consolidatedSessionTimeoutComponent$ | async) {
<bit-nav-item [text]="'appearance' | i18n" route="settings/appearance"></bit-nav-item>
} @else {
<bit-nav-item [text]="'preferences' | i18n" route="settings/preferences"></bit-nav-item>
}
<bit-nav-item
[text]="'subscription' | i18n"
route="settings/subscription"

View File

@@ -42,6 +42,7 @@ export class UserLayoutComponent implements OnInit {
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
protected showSponsoredFamilies$: Observable<boolean>;
protected showSubscription$: Observable<boolean>;
protected consolidatedSessionTimeoutComponent$: Observable<boolean>;
constructor(
private syncService: SyncService,
@@ -74,6 +75,10 @@ export class UserLayoutComponent implements OnInit {
}),
),
);
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
);
}
async ngOnInit() {

View File

@@ -14,6 +14,7 @@ import {
import { LoginViaWebAuthnComponent } from "@bitwarden/angular/auth/login-via-webauthn/login-via-webauthn.component";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
DevicesIcon,
RegistrationUserAddIcon,
@@ -48,6 +49,7 @@ import {
NewDeviceVerificationComponent,
} from "@bitwarden/auth/angular";
import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui";
import { premiumInterestRedirectGuard } from "@bitwarden/web-vault/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard";
@@ -82,6 +84,7 @@ import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
import { UserLayoutComponent } from "./layouts/user-layout.component";
import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component";
import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm-landing.component";
import { AppearanceComponent } from "./settings/appearance.component";
import { DomainRulesComponent } from "./settings/domain-rules.component";
import { PreferencesComponent } from "./settings/preferences.component";
import { CredentialGeneratorComponent } from "./tools/credential-generator/credential-generator.component";
@@ -663,9 +666,30 @@ const routes: Routes = [
component: AccountComponent,
data: { titleId: "myAccount" } satisfies RouteDataProperties,
},
{
path: "appearance",
component: AppearanceComponent,
canActivate: [
canAccessFeature(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
true,
"/settings/preferences",
false,
),
],
data: { titleId: "appearance" } satisfies RouteDataProperties,
},
{
path: "preferences",
component: PreferencesComponent,
canActivate: [
canAccessFeature(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
false,
"/settings/appearance",
false,
),
],
data: { titleId: "preferences" } satisfies RouteDataProperties,
},
{

View File

@@ -0,0 +1,48 @@
<app-header></app-header>
<bit-container>
<form [formGroup]="form" class="tw-w-full tw-max-w-md">
<bit-form-field>
<bit-label>{{ "theme" | i18n }}</bit-label>
<bit-select formControlName="theme" id="theme">
@for (option of themeOptions; track option.value) {
<bit-option [value]="option.value" [label]="option.name"></bit-option>
}
</bit-select>
<bit-hint>{{ "themeDesc" | i18n }}</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>
{{ "language" | i18n }}
<a
bitLink
class="tw-float-right"
href="https://bitwarden.com/help/localization/"
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMoreAboutLocalization' | i18n }}"
slot="end"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<bit-select formControlName="locale" id="locale">
@for (option of localeOptions; track option.value) {
<bit-option [value]="option.value" [label]="option.name"></bit-option>
}
</bit-select>
<bit-hint>{{ "languageDesc" | i18n }}</bit-hint>
</bit-form-field>
<div class="tw-flex tw-items-start tw-gap-1.5">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="enableFavicons" />
<bit-label>
{{ "showIconsChangePasswordUrls" | i18n }}
</bit-label>
</bit-form-control>
<div class="-tw-mt-0.5">
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
</div>
</div>
</form>
</bit-container>

View File

@@ -0,0 +1,215 @@
import { ComponentFixture, fakeAsync, flush, TestBed } from "@angular/core/testing";
import { ReactiveFormsModule } from "@angular/forms";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { AppearanceComponent } from "./appearance.component";
describe("AppearanceComponent", () => {
let component: AppearanceComponent;
let fixture: ComponentFixture<AppearanceComponent>;
let mockI18nService: MockProxy<I18nService>;
let mockThemeStateService: MockProxy<ThemeStateService>;
let mockDomainSettingsService: MockProxy<DomainSettingsService>;
const mockShowFavicons$ = new BehaviorSubject<boolean>(true);
const mockSelectedTheme$ = new BehaviorSubject<Theme>(ThemeTypes.Light);
const mockUserSetLocale$ = new BehaviorSubject<string | undefined>("en");
const mockSupportedLocales = ["en", "es", "fr", "de"];
const mockLocaleNames = new Map([
["en", "English"],
["es", "Español"],
["fr", "Français"],
["de", "Deutsch"],
]);
beforeEach(async () => {
mockI18nService = mock<I18nService>();
mockThemeStateService = mock<ThemeStateService>();
mockDomainSettingsService = mock<DomainSettingsService>();
mockI18nService.supportedTranslationLocales = mockSupportedLocales;
mockI18nService.localeNames = mockLocaleNames;
mockI18nService.collator = {
compare: jest.fn((a: string, b: string) => a.localeCompare(b)),
} as any;
mockI18nService.t.mockImplementation((key: string) => `${key}-used-i18n`);
mockI18nService.userSetLocale$ = mockUserSetLocale$;
mockThemeStateService.selectedTheme$ = mockSelectedTheme$;
mockDomainSettingsService.showFavicons$ = mockShowFavicons$;
mockDomainSettingsService.setShowFavicons.mockResolvedValue(undefined);
mockThemeStateService.setSelectedTheme.mockResolvedValue(undefined);
mockI18nService.setLocale.mockResolvedValue(undefined);
await TestBed.configureTestingModule({
imports: [AppearanceComponent, ReactiveFormsModule, NoopAnimationsModule],
providers: [
{ provide: I18nService, useValue: mockI18nService },
{ provide: ThemeStateService, useValue: mockThemeStateService },
{ provide: DomainSettingsService, useValue: mockDomainSettingsService },
],
})
.overrideComponent(AppearanceComponent, {
set: {
template: "",
imports: [],
},
})
.compileComponents();
fixture = TestBed.createComponent(AppearanceComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should create", () => {
expect(component).toBeTruthy();
});
describe("constructor", () => {
describe("locale options setup", () => {
it("should create locale options sorted by name from supported locales with display names", () => {
expect(component.localeOptions).toHaveLength(5);
expect(component.localeOptions[0]).toEqual({ name: "default-used-i18n", value: null });
expect(component.localeOptions[1]).toEqual({ name: "de - Deutsch", value: "de" });
expect(component.localeOptions[2]).toEqual({ name: "en - English", value: "en" });
expect(component.localeOptions[3]).toEqual({ name: "es - Español", value: "es" });
expect(component.localeOptions[4]).toEqual({ name: "fr - Français", value: "fr" });
});
});
describe("theme options setup", () => {
it("should create theme options with Light, Dark, and System", () => {
expect(component.themeOptions).toEqual([
{ name: "themeLight-used-i18n", value: ThemeTypes.Light },
{ name: "themeDark-used-i18n", value: ThemeTypes.Dark },
{ name: "themeSystem-used-i18n", value: ThemeTypes.System },
]);
});
});
});
describe("ngOnInit", () => {
it("should initialize form with values", fakeAsync(() => {
mockShowFavicons$.next(false);
mockSelectedTheme$.next(ThemeTypes.Dark);
mockUserSetLocale$.next("es");
fixture.detectChanges();
flush();
expect(component.form.value).toEqual({
enableFavicons: false,
theme: ThemeTypes.Dark,
locale: "es",
});
}));
it("should set locale to null when user locale not set", fakeAsync(() => {
mockUserSetLocale$.next(undefined);
fixture.detectChanges();
flush();
expect(component.form.value.locale).toBeNull();
}));
});
describe("enableFavicons value changes", () => {
beforeEach(fakeAsync(() => {
fixture.detectChanges();
flush();
jest.clearAllMocks();
}));
it("should call setShowFavicons when enableFavicons changes to true", fakeAsync(() => {
component.form.controls.enableFavicons.setValue(true);
flush();
expect(mockDomainSettingsService.setShowFavicons).toHaveBeenCalledWith(true);
}));
it("should call setShowFavicons when enableFavicons changes to false", fakeAsync(() => {
component.form.controls.enableFavicons.setValue(false);
flush();
expect(mockDomainSettingsService.setShowFavicons).toHaveBeenCalledWith(false);
}));
it("should not call setShowFavicons when value is null", fakeAsync(() => {
component.form.controls.enableFavicons.setValue(null);
flush();
expect(mockDomainSettingsService.setShowFavicons).not.toHaveBeenCalled();
}));
});
describe("theme value changes", () => {
beforeEach(fakeAsync(() => {
fixture.detectChanges();
flush();
jest.clearAllMocks();
}));
it.each([ThemeTypes.Light, ThemeTypes.Dark, ThemeTypes.System])(
"should call setSelectedTheme when theme changes to %s",
fakeAsync((themeType: Theme) => {
component.form.controls.theme.setValue(themeType);
flush();
expect(mockThemeStateService.setSelectedTheme).toHaveBeenCalledWith(themeType);
}),
);
it("should not call setSelectedTheme when value is null", fakeAsync(() => {
component.form.controls.theme.setValue(null);
flush();
expect(mockThemeStateService.setSelectedTheme).not.toHaveBeenCalled();
}));
});
describe("locale value changes", () => {
let reloadMock: jest.Mock;
beforeEach(fakeAsync(() => {
reloadMock = jest.fn();
Object.defineProperty(window, "location", {
value: { reload: reloadMock },
writable: true,
});
fixture.detectChanges();
flush();
jest.clearAllMocks();
}));
it("should call setLocale and reload window when locale changes to english", fakeAsync(() => {
component.form.controls.locale.setValue("es");
flush();
expect(mockI18nService.setLocale).toHaveBeenCalledWith("es");
expect(reloadMock).toHaveBeenCalled();
}));
it("should call setLocale and reload window when locale changes to default", fakeAsync(() => {
component.form.controls.locale.setValue(null);
flush();
expect(mockI18nService.setLocale).toHaveBeenCalledWith(null);
expect(reloadMock).toHaveBeenCalled();
}));
});
});

View File

@@ -0,0 +1,107 @@
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder } from "@angular/forms";
import { filter, firstValueFrom, switchMap } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
import { HeaderModule } from "../layouts/header/header.module";
import { SharedModule } from "../shared";
type LocaleOption = {
name: string;
value: string | null;
};
type ThemeOption = {
name: string;
value: Theme;
};
@Component({
selector: "app-appearance",
templateUrl: "appearance.component.html",
imports: [SharedModule, HeaderModule, PermitCipherDetailsPopoverComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppearanceComponent implements OnInit {
localeOptions: LocaleOption[];
themeOptions: ThemeOption[];
form = this.formBuilder.group({
enableFavicons: true,
theme: [ThemeTypes.Light as Theme],
locale: [null as string | null],
});
constructor(
private formBuilder: FormBuilder,
private i18nService: I18nService,
private themeStateService: ThemeStateService,
private domainSettingsService: DomainSettingsService,
private destroyRef: DestroyRef,
) {
const localeOptions: LocaleOption[] = [];
i18nService.supportedTranslationLocales.forEach((locale) => {
let name = locale;
if (i18nService.localeNames.has(locale)) {
name += " - " + i18nService.localeNames.get(locale);
}
localeOptions.push({ name: name, value: locale });
});
localeOptions.sort(Utils.getSortFunction(i18nService, "name"));
localeOptions.splice(0, 0, { name: i18nService.t("default"), value: null });
this.localeOptions = localeOptions;
this.themeOptions = [
{ name: i18nService.t("themeLight"), value: ThemeTypes.Light },
{ name: i18nService.t("themeDark"), value: ThemeTypes.Dark },
{ name: i18nService.t("themeSystem"), value: ThemeTypes.System },
];
}
async ngOnInit() {
this.form.setValue(
{
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
locale: (await firstValueFrom(this.i18nService.userSetLocale$)) ?? null,
},
{ emitEvent: false },
);
this.form.controls.enableFavicons.valueChanges
.pipe(
filter((enableFavicons) => enableFavicons != null),
switchMap(async (enableFavicons) => {
await this.domainSettingsService.setShowFavicons(enableFavicons);
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();
this.form.controls.theme.valueChanges
.pipe(
filter((theme) => theme != null),
switchMap(async (theme) => {
await this.themeStateService.setSelectedTheme(theme);
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();
this.form.controls.locale.valueChanges
.pipe(
switchMap(async (locale) => {
await this.i18nService.setLocale(locale);
window.location.reload();
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();
}
}

View File

@@ -48,8 +48,8 @@
</bit-radio-group>
</ng-container>
<bit-form-field>
<bit-label
>{{ "language" | i18n }}
<bit-label>
{{ "language" | i18n }}
<a
bitLink
class="tw-float-right"

View File

@@ -39,6 +39,11 @@ import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
import { HeaderModule } from "../layouts/header/header.module";
import { SharedModule } from "../shared";
/**
* @deprecated Use {@link AppearanceComponent} and {@link SessionTimeoutComponent} instead.
*
* TODO Cleanup once feature flag enabled: https://bitwarden.atlassian.net/browse/PM-27297
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -211,6 +216,8 @@ export class PreferencesComponent implements OnInit, OnDestroy {
values.vaultTimeout,
values.vaultTimeoutAction,
);
// Save other preferences (theme, locale, favicons)
await this.domainSettingsService.setShowFavicons(values.enableFavicons);
await this.themeStateService.setSelectedTheme(values.theme);
await this.i18nService.setLocale(values.locale);