1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 10:43:35 +00:00

[PM-23524] Port desktop settings to CL vault timeout, and drop old non-CL vault timeout components (#15513)

* Remove unused old vault timeout component

* Drop desktop specific vault timeout component and replace it with shared CL implementation

* Fix tests

* Fix test

* Fix build on desktop

* Fix tests

* Fix margin
This commit is contained in:
Bernd Schoolmann
2025-07-28 16:17:53 +02:00
committed by GitHub
parent b0ffaf0b18
commit 38d5edc2c5
10 changed files with 205 additions and 299 deletions

View File

@@ -1,19 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, Observable, Subject, firstValueFrom, of } from "rxjs";
import {
concatMap,
debounceTime,
filter,
map,
switchMap,
takeUntil,
tap,
timeout,
} from "rxjs/operators";
import { CommonModule } from "@angular/common";
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { BehaviorSubject, Observable, Subject, combineLatest, firstValueFrom, of } from "rxjs";
import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -43,7 +38,19 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.e
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import {
CheckboxModule,
DialogService,
FormFieldModule,
IconButtonModule,
ItemModule,
LinkModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
import { SetPinComponent } from "../../auth/components/set-pin.component";
@@ -57,14 +64,30 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
@Component({
selector: "app-settings",
templateUrl: "settings.component.html",
standalone: false,
standalone: true,
imports: [
CheckboxModule,
CommonModule,
FormFieldModule,
FormsModule,
ReactiveFormsModule,
IconButtonModule,
ItemModule,
JslibModule,
LinkModule,
RouterModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
TypographyModule,
VaultTimeoutInputComponent,
],
})
export class SettingsComponent implements OnInit, OnDestroy {
// For use in template
protected readonly VaultTimeoutAction = VaultTimeoutAction;
showMinToTray = false;
vaultTimeoutOptions: VaultTimeoutOption[] = [];
localeOptions: any[];
themeOptions: any[];
clearClipboardOptions: any[];
@@ -96,12 +119,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
currentUserEmail: string;
currentUserId: UserId;
availableVaultTimeoutActions$: Observable<VaultTimeoutAction[]>;
vaultTimeoutPolicyCallout: Observable<{
timeout: { hours: number; minutes: number };
action: "lock" | "logOut";
}>;
previousVaultTimeout: VaultTimeout = null;
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutOptions: VaultTimeoutOption[] = [];
hasVaultTimeoutPolicy = false;
userHasMasterPassword: boolean;
userHasPinSet: boolean;
@@ -170,6 +190,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
private nativeMessagingManifestService: NativeMessagingManifestService,
private configService: ConfigService,
private validationService: ValidationService,
private changeDetectorRef: ChangeDetectorRef,
private toastService: ToastService,
) {
this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
@@ -255,38 +277,54 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.currentUserEmail = activeAccount.email;
this.currentUserId = activeAccount.id;
this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe(
switchMap(() =>
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(activeAccount.id),
),
);
// Load timeout policy
this.vaultTimeoutPolicyCallout = this.accountService.activeAccount$.pipe(
const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
),
getFirstPolicy,
filter((policy) => policy != null),
map((policy) => {
let timeout;
if (policy.data?.minutes) {
timeout = {
hours: Math.floor(policy.data?.minutes / 60),
minutes: policy.data?.minutes % 60,
};
}
return { timeout: timeout, action: policy.data?.action };
}),
tap((policy) => {
if (policy.action) {
);
if ((await firstValueFrom(maximumVaultTimeoutPolicy)) != null) {
this.hasVaultTimeoutPolicy = true;
}
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, action]) => {
this.availableVaultTimeoutActions = availableActions;
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
// NOTE: The UI doesn't properly update without detect changes.
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
// meaning that we are forced to use regular class variables instead of observables.
this.changeDetectorRef.detectChanges();
});
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
maximumVaultTimeoutPolicy,
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, policy]) => {
if (policy?.data?.action || availableActions.length <= 1) {
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else {
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
}
}),
);
});
// Load initial values
this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id);
@@ -354,7 +392,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Non-form values
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.previousVaultTimeout = this.form.value.vaultTimeout;
this.refreshTimeoutSettings$
.pipe(
@@ -370,9 +407,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Form events
this.form.controls.vaultTimeout.valueChanges
.pipe(
debounceTime(500),
concatMap(async (value) => {
await this.saveVaultTimeout(value);
startWith(initialValues.vaultTimeout), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeout(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
@@ -423,7 +461,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
}, 1000);
}
async saveVaultTimeout(newValue: VaultTimeout) {
async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
if (newValue === VaultTimeoutStringType.Never) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
@@ -432,7 +470,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
});
if (!confirmed) {
this.form.controls.vaultTimeout.setValue(this.previousVaultTimeout);
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
return;
}
}
@@ -451,8 +489,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
return;
}
this.previousVaultTimeout = this.form.value.vaultTimeout;
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
@@ -460,10 +496,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
newValue,
this.form.getRawValue().vaultTimeoutAction,
);
this.refreshTimeoutSettings$.next();
}
async saveVaultTimeoutAction(newValue: VaultTimeoutAction) {
if (newValue === "logOut") {
async saveVaultTimeoutAction(value: VaultTimeoutAction) {
if (value === VaultTimeoutAction.LogOut) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
content: { key: "vaultTimeoutLogOutConfirmation" },
@@ -471,7 +508,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
});
if (!confirmed) {
this.form.controls.vaultTimeoutAction.patchValue(VaultTimeoutAction.Lock, {
this.form.controls.vaultTimeoutAction.setValue(VaultTimeoutAction.Lock, {
emitEvent: false,
});
return;
@@ -479,11 +516,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("vaultTimeoutTooLarge"),
});
return;
}
@@ -492,8 +529,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
this.form.value.vaultTimeout,
newValue,
value,
);
this.refreshTimeoutSettings$.next();
}
async updatePinHandler(value: boolean) {