From f7ac4bda6489f156f1ce9f0b5459de48f507c358 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Thu, 13 Aug 2026 17:01:05 +0200 Subject: [PATCH 1/5] Remove conditional code for desktop-ui-settings-dialog feature flag --- apps/desktop/src/app/app.component.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 48ce5a44d452..6a90e87353eb 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -81,7 +81,6 @@ import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater"; import { SSO_COOKIE_VENDOR_CALLBACK_COMMAND } from "../platform/services/server-communication-config/server-communication-config-platform-api.service"; import { SettingsDialogComponent } from "./accounts/settings-dialog.component"; -import { SettingsComponent } from "./accounts/settings.component"; import { ExportDesktopComponent } from "./tools/export/export-desktop.component"; import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component"; import { ImportDesktopComponent } from "./tools/import/import-desktop.component"; @@ -118,9 +117,6 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours standalone: false, }) export class AppComponent implements OnInit, OnDestroy { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef; @@ -295,11 +291,7 @@ export class AppComponent implements OnInit, OnDestroy { } break; case "openSettings": { - if (await this.configService.getFeatureFlag(FeatureFlag.DesktopSettingsDialog)) { - SettingsDialogComponent.open(this.dialogService); - } else { - await this.openModal(SettingsComponent, this.settingsRef); - } + SettingsDialogComponent.open(this.dialogService); break; } case "openTroubleshootingDialog": From 335fd643d4d10d697d088980b1cbe172f67e34fd Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Thu, 13 Aug 2026 17:01:46 +0200 Subject: [PATCH 2/5] Delete now unused settings.component --- .../src/app/accounts/settings.component.html | 384 -------- .../app/accounts/settings.component.spec.ts | 916 ------------------ .../src/app/accounts/settings.component.ts | 695 ------------- 3 files changed, 1995 deletions(-) delete mode 100644 apps/desktop/src/app/accounts/settings.component.html delete mode 100644 apps/desktop/src/app/accounts/settings.component.spec.ts delete mode 100644 apps/desktop/src/app/accounts/settings.component.ts diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html deleted file mode 100644 index cb49593244a1..000000000000 --- a/apps/desktop/src/app/accounts/settings.component.html +++ /dev/null @@ -1,384 +0,0 @@ - diff --git a/apps/desktop/src/app/accounts/settings.component.spec.ts b/apps/desktop/src/app/accounts/settings.component.spec.ts deleted file mode 100644 index 53b3f5be6b55..000000000000 --- a/apps/desktop/src/app/accounts/settings.component.spec.ts +++ /dev/null @@ -1,916 +0,0 @@ -import { ChangeDetectionStrategy, Component, input, NO_ERRORS_SCHEMA } from "@angular/core"; -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { By } from "@angular/platform-browser"; -import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { DeviceType } from "@bitwarden/common/enums"; -import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { MessageSender } from "@bitwarden/common/platform/messaging"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "@bitwarden/common/types/key"; -import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; -import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management"; -import { SessionTimeoutSettingsComponent } from "@bitwarden/key-management-ui"; -// eslint-disable-next-line no-restricted-imports -import { SymmetricCryptoKey } from "@bitwarden/legacy-crypto"; -import { VaultCopyButtonsService } from "@bitwarden/vault"; - -import { SetPinComponent } from "../../auth/components/set-pin.component"; -import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; -import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; -import { DesktopAutotypeMvpService } from "../../autofill/services/desktop-autotype-mvp.service"; -import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service"; -import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; -import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; - -import { SettingsComponent } from "./settings.component"; - -@Component({ - selector: "bit-session-timeout-settings", - standalone: true, - template: "", - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class MockSessionTimeoutSettingsComponent { - readonly refreshTimeoutActionSettings = input(); -} - -describe("SettingsComponent", () => { - let component: SettingsComponent; - let fixture: ComponentFixture; - let originalIpc: any; - - const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const vaultTimeoutSettingsService = mock(); - const biometricStateService = mock(); - const policyService = mock(); - const i18nService = mock(); - const autofillSettingsServiceAbstraction = mock(); - const desktopSettingsService = mock(); - const domainSettingsService = mock(); - const desktopAutofillSettingsService = mock(); - const themeStateService = mock(); - const pinServiceAbstraction = mock(); - const desktopBiometricsService = mock(); - const platformUtilsService = mock(); - const logService = mock(); - const validationService = mock(); - const messagingService = mock(); - const keyService = mock(); - const dialogService = mock(); - const desktopAutotypeMvpService = mock(); - const billingAccountProfileStateService = mock(); - const configService = mock(); - const userVerificationService = mock(); - const vaultCopyButtonsService = mock(); - - const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as unknown as UserKey; - - beforeEach(async () => { - jest.clearAllMocks(); - - originalIpc = (global as any).ipc; - (global as any).ipc = { - auth: { - loginRequest: jest.fn(), - }, - platform: { - isDev: false, - isWindowsStore: false, - powermonitor: { - isLockMonitorAvailable: async () => false, - }, - }, - }; - - i18nService.supportedTranslationLocales = []; - i18nService.t.mockImplementation((key: string) => key); - - configService.getFeatureFlag$.mockReturnValue(of(false)); - - await TestBed.configureTestingModule({ - imports: [], - providers: [ - { - provide: AutofillSettingsServiceAbstraction, - useValue: autofillSettingsServiceAbstraction, - }, - { provide: AccountService, useValue: accountService }, - { provide: BiometricStateService, useValue: biometricStateService }, - { provide: ConfigService, useValue: configService }, - { - provide: DesktopAutofillSettingsService, - useValue: desktopAutofillSettingsService, - }, - { provide: DesktopBiometricsService, useValue: desktopBiometricsService }, - { provide: DesktopSettingsService, useValue: desktopSettingsService }, - { provide: DomainSettingsService, useValue: domainSettingsService }, - { provide: DialogService, useValue: dialogService }, - { provide: I18nService, useValue: i18nService }, - { provide: LogService, useValue: logService }, - { provide: MessageSender, useValue: mock() }, - { - provide: NativeMessagingManifestService, - useValue: mock(), - }, - { provide: KeyService, useValue: keyService }, - { provide: PinServiceAbstraction, useValue: pinServiceAbstraction }, - { provide: PlatformUtilsService, useValue: platformUtilsService }, - { provide: PolicyService, useValue: policyService }, - { provide: StateService, useValue: mock() }, - { provide: ThemeStateService, useValue: themeStateService }, - { provide: UserVerificationService, useValue: userVerificationService }, - { provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService }, - { provide: ValidationService, useValue: validationService }, - { provide: MessagingService, useValue: messagingService }, - { provide: ToastService, useValue: mock() }, - { provide: DesktopAutotypeMvpService, useValue: desktopAutotypeMvpService }, - { provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService }, - { provide: VaultCopyButtonsService, useValue: vaultCopyButtonsService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - TestBed.overrideComponent(SettingsComponent, { - add: { - imports: [MockSessionTimeoutSettingsComponent], - providers: [ - { - provide: DialogService, - useValue: dialogService, - }, - ], - }, - remove: { - imports: [SessionTimeoutSettingsComponent], - providers: [DialogService], - }, - }); - - fixture = TestBed.createComponent(SettingsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - - desktopBiometricsService.hasPersistentKey.mockResolvedValue(false); - vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(false); - biometricStateService.promptAutomatically$.mockReturnValue(of(false)); - autofillSettingsServiceAbstraction.clearClipboardDelay$ = of(null); - desktopSettingsService.minimizeOnCopy$ = of(false); - desktopSettingsService.runInBackground$ = of(false); - desktopSettingsService.openAtLogin$ = of(false); - desktopSettingsService.hardwareAcceleration$ = of(false); - desktopSettingsService.sshAgentEnabled$ = of(false); - desktopSettingsService.sshAgentPromptBehavior$ = of(SshAgentPromptType.Always); - desktopSettingsService.preventScreenshots$ = of(false); - domainSettingsService.showFavicons$ = of(false); - desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$ = of(false); - vaultCopyButtonsService.showQuickCopyActions$ = of(false); - themeStateService.selectedTheme$ = of(ThemeType.System); - i18nService.userSetLocale$ = of("en"); - pinServiceAbstraction.isPinSet.mockResolvedValue(false); - policyService.policiesByType$.mockReturnValue(of([null])); - desktopAutotypeMvpService.autotypeEnabledUserSetting$ = of(false); - desktopAutotypeMvpService.autotypeKeyboardShortcut$ = of(["Control", "Alt", "B"]); - billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); - }); - - afterEach(() => { - (global as any).ipc = originalIpc; - }); - - it("pin enabled when RemoveUnlockWithPin policy is not set", async () => { - // @ts-strict-ignore - policyService.policiesByType$.mockReturnValue(of([null])); - - await component.ngOnInit(); - - await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(true); - }); - - it("pin enabled when RemoveUnlockWithPin policy is disabled", async () => { - const policy = new Policy(); - policy.type = PolicyType.RemoveUnlockWithPin; - policy.enabled = false; - policyService.policiesByType$.mockReturnValue(of([policy])); - - await component.ngOnInit(); - - await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(true); - }); - - it("pin disabled when RemoveUnlockWithPin policy is enabled", async () => { - const policy = new Policy(); - policy.type = PolicyType.RemoveUnlockWithPin; - policy.enabled = true; - policyService.policiesByType$.mockReturnValue(of([policy])); - - await component.ngOnInit(); - - await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(false); - }); - - it("pin visible when RemoveUnlockWithPin policy is not set", async () => { - // @ts-strict-ignore - policyService.policiesByType$.mockReturnValue(of([null])); - - await component.ngOnInit(); - fixture.detectChanges(); - - const pinInputElement = fixture.debugElement.query(By.css("#pin")); - expect(pinInputElement).not.toBeNull(); - expect(pinInputElement.name).toBe("input"); - expect(pinInputElement.attributes).toMatchObject({ - type: "checkbox", - }); - }); - - it("pin visible when RemoveUnlockWithPin policy is disabled", async () => { - const policy = new Policy(); - policy.type = PolicyType.RemoveUnlockWithPin; - policy.enabled = false; - policyService.policiesByType$.mockReturnValue(of([policy])); - - await component.ngOnInit(); - fixture.detectChanges(); - - const pinInputElement = fixture.debugElement.query(By.css("#pin")); - expect(pinInputElement).not.toBeNull(); - expect(pinInputElement.name).toBe("input"); - expect(pinInputElement.attributes).toMatchObject({ - type: "checkbox", - }); - }); - - it("pin visible when RemoveUnlockWithPin policy is enabled and pin set", async () => { - const policy = new Policy(); - policy.type = PolicyType.RemoveUnlockWithPin; - policy.enabled = true; - policyService.policiesByType$.mockReturnValue(of([policy])); - pinServiceAbstraction.isPinSet.mockResolvedValue(true); - - await component.ngOnInit(); - fixture.detectChanges(); - - const pinInputElement = fixture.debugElement.query(By.css("#pin")); - expect(pinInputElement).not.toBeNull(); - expect(pinInputElement.name).toBe("input"); - expect(pinInputElement.attributes).toMatchObject({ - type: "checkbox", - }); - }); - - it("pin not visible when RemoveUnlockWithPin policy is enabled", async () => { - const policy = new Policy(); - policy.type = PolicyType.RemoveUnlockWithPin; - policy.enabled = true; - policyService.policiesByType$.mockReturnValue(of([policy])); - - await component.ngOnInit(); - fixture.detectChanges(); - - const pinInputElement = fixture.debugElement.query(By.css("#pin")); - expect(pinInputElement).toBeNull(); - }); - - describe("biometrics enabled", () => { - beforeEach(() => { - desktopBiometricsService.getBiometricsStatus.mockResolvedValue(BiometricsStatus.Available); - desktopBiometricsService.canEnableBiometricUnlock.mockResolvedValue(true); - vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(true); - }); - - describe("windows desktop", () => { - beforeEach(() => { - platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - - // Recreate component to apply the correct device - fixture = TestBed.createComponent(SettingsComponent); - component = fixture.componentInstance; - }); - - test.each([true, false])( - `correct message display for require MP/PIN on app restart when pin is set, windows desktop, and policy is %s`, - async (policyEnabled) => { - const policy = new Policy(); - policy.type = PolicyType.RemoveUnlockWithPin; - policy.enabled = policyEnabled; - policyService.policiesByType$.mockReturnValue(of([policy])); - platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - pinServiceAbstraction.isPinSet.mockResolvedValue(true); - - await component.ngOnInit(); - fixture.detectChanges(); - - const textNodes = checkRequireMasterPasswordOnAppRestartElement(fixture); - - if (policyEnabled) { - expect(textNodes).toContain("requireMasterPasswordOnAppRestart"); - } else { - expect(textNodes).toContain("requireMasterPasswordOrPinOnAppRestart"); - } - }, - ); - - describe("users without a master password", () => { - beforeEach(() => { - userVerificationService.hasMasterPassword.mockResolvedValue(false); - }); - - it("displays require MP/PIN on app restart checkbox when pin is set", async () => { - pinServiceAbstraction.isPinSet.mockResolvedValue(true); - - await component.ngOnInit(); - fixture.detectChanges(); - - checkRequireMasterPasswordOnAppRestartElement(fixture); - }); - - it("does not display require MP/PIN on app restart checkbox when pin is not set", async () => { - pinServiceAbstraction.isPinSet.mockResolvedValue(false); - - await component.ngOnInit(); - fixture.detectChanges(); - - const requireMasterPasswordOnAppRestartLabelElement = fixture.debugElement.query( - By.css("label[for='requireMasterPasswordOnAppRestart']"), - ); - expect(requireMasterPasswordOnAppRestartLabelElement).toBeNull(); - }); - }); - - function checkRequireMasterPasswordOnAppRestartElement( - fixture: ComponentFixture, - ) { - const requireMasterPasswordOnAppRestartLabelElement = fixture.debugElement.query( - By.css("label[for='requireMasterPasswordOnAppRestart']"), - ); - expect(requireMasterPasswordOnAppRestartLabelElement).not.toBeNull(); - expect(requireMasterPasswordOnAppRestartLabelElement.children).toHaveLength(1); - expect(requireMasterPasswordOnAppRestartLabelElement.children[0].name).toBe("input"); - expect(requireMasterPasswordOnAppRestartLabelElement.children[0].attributes).toMatchObject({ - id: "requireMasterPasswordOnAppRestart", - type: "checkbox", - }); - const textNodes = requireMasterPasswordOnAppRestartLabelElement.childNodes - .filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE) - .map((node) => node.nativeNode.wholeText?.trim()); - return textNodes; - } - }); - }); - - describe("updatePinHandler", () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - test.each([true, false])(`handles thrown errors when updated pin to %s`, async (update) => { - const error = new Error("Test error"); - jest.spyOn(component, "updatePin").mockRejectedValue(error); - - await component.ngOnInit(); - await component.updatePinHandler(update); - - expect(logService.error).toHaveBeenCalled(); - expect(component.form.controls.pin.value).toBe(!update); - expect(validationService.showError).toHaveBeenCalledWith(error); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - describe("when updating to true", () => { - it("sets pin form control to false when the PIN dialog is cancelled", async () => { - jest.spyOn(SetPinComponent, "open").mockReturnValue(null); - - await component.ngOnInit(); - await component.updatePinHandler(true); - - expect(component.form.controls.pin.value).toBe(false); - expect(pinServiceAbstraction.unsetPin).not.toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - test.each([true, false])( - `sets the pin form control to the dialog result`, - async (dialogResult) => { - const mockDialogRef = { - closed: of(dialogResult), - } as DialogRef; - jest.spyOn(SetPinComponent, "open").mockReturnValue(mockDialogRef); - - await component.ngOnInit(); - await component.updatePinHandler(true); - - expect(component.form.controls.pin.value).toBe(dialogResult); - expect(pinServiceAbstraction.unsetPin).not.toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }, - ); - }); - - describe("when updating to false", () => { - it("sets the pin form control to false and clears vault timeout", async () => { - await component.ngOnInit(); - await component.updatePinHandler(false); - - expect(component.form.controls.pin.value).toBe(false); - expect(pinServiceAbstraction.unsetPin).toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - describe("when windows biometric v2 feature flag is enabled", () => { - beforeEach(() => { - keyService.userKey$.mockReturnValue(of(mockUserKey)); - }); - - test.each([false, true])( - "enrolls persistent biometric if needed, enrolled is %s", - async (enrolled) => { - desktopBiometricsService.hasPersistentKey.mockResolvedValue(enrolled); - - await component.ngOnInit(); - component.isWindows = true; - component.form.value.requireMasterPasswordOnAppRestart = true; - component.userHasMasterPassword = false; - component.supportsBiometric = true; - component.form.value.biometric = true; - - await component.updatePinHandler(false); - - expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(false); - expect(component.form.controls.pin.value).toBe(false); - expect(pinServiceAbstraction.unsetPin).toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - - if (enrolled) { - expect(desktopBiometricsService.enrollPersistent).not.toHaveBeenCalled(); - } else { - expect(desktopBiometricsService.enrollPersistent).toHaveBeenCalledWith( - mockUserId, - mockUserKey, - ); - } - }, - ); - - test.each([ - { - userHasMasterPassword: true, - supportsBiometric: false, - biometric: false, - requireMasterPasswordOnAppRestart: false, - }, - { - userHasMasterPassword: true, - supportsBiometric: false, - biometric: false, - requireMasterPasswordOnAppRestart: true, - }, - { - userHasMasterPassword: true, - supportsBiometric: false, - biometric: true, - requireMasterPasswordOnAppRestart: false, - }, - { - userHasMasterPassword: true, - supportsBiometric: false, - biometric: true, - requireMasterPasswordOnAppRestart: true, - }, - { - userHasMasterPassword: true, - supportsBiometric: true, - biometric: false, - requireMasterPasswordOnAppRestart: false, - }, - { - userHasMasterPassword: true, - supportsBiometric: true, - biometric: false, - requireMasterPasswordOnAppRestart: true, - }, - { - userHasMasterPassword: false, - supportsBiometric: false, - biometric: false, - requireMasterPasswordOnAppRestart: false, - }, - { - userHasMasterPassword: false, - supportsBiometric: false, - biometric: false, - requireMasterPasswordOnAppRestart: true, - }, - { - userHasMasterPassword: false, - supportsBiometric: false, - biometric: true, - requireMasterPasswordOnAppRestart: false, - }, - { - userHasMasterPassword: false, - supportsBiometric: false, - biometric: true, - requireMasterPasswordOnAppRestart: true, - }, - { - userHasMasterPassword: false, - supportsBiometric: true, - biometric: false, - requireMasterPasswordOnAppRestart: false, - }, - { - userHasMasterPassword: false, - supportsBiometric: true, - biometric: false, - requireMasterPasswordOnAppRestart: true, - }, - ])( - "does not enroll persistent biometric when conditions are not met: userHasMasterPassword=$userHasMasterPassword, supportsBiometric=$supportsBiometric, biometric=$biometric, requireMasterPasswordOnAppRestart=$requireMasterPasswordOnAppRestart", - async ({ - userHasMasterPassword, - supportsBiometric, - biometric, - requireMasterPasswordOnAppRestart, - }) => { - desktopBiometricsService.hasPersistentKey.mockResolvedValue(false); - - await component.ngOnInit(); - component.isWindows = true; - component.form.value.requireMasterPasswordOnAppRestart = - requireMasterPasswordOnAppRestart; - component.userHasMasterPassword = userHasMasterPassword; - component.supportsBiometric = supportsBiometric; - component.form.value.biometric = biometric; - - await component.updatePinHandler(false); - - expect(component.form.controls.pin.value).toBe(false); - expect(pinServiceAbstraction.unsetPin).toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - expect(desktopBiometricsService.enrollPersistent).not.toHaveBeenCalled(); - }, - ); - }); - }); - }); - - describe("updateBiometricHandler", () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - test.each([true, false])( - `handles thrown errors when updated biometrics to %s`, - async (update) => { - const error = new Error("Test error"); - jest.spyOn(component, "updateBiometric").mockRejectedValue(error); - - await component.ngOnInit(); - await component.updateBiometricHandler(update); - - expect(logService.error).toHaveBeenCalled(); - expect(component.form.controls.biometric.value).toBe(false); - expect(validationService.showError).toHaveBeenCalledWith(error); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }, - ); - - describe("when updating to true", () => { - beforeEach(async () => { - await component.ngOnInit(); - component.supportsBiometric = true; - }); - - it("calls services to clear biometrics when supportsBiometric is false", async () => { - component.supportsBiometric = false; - await component.updateBiometricHandler(true); - - expect(component.form.controls.biometric.value).toBe(false); - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenLastCalledWith( - false, - mockUserId, - ); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - test.each([true, false])( - `launches a dialog and exits when man setup is needed, dialog result is %s`, - async (dialogResult) => { - dialogService.openSimpleDialog.mockResolvedValue(dialogResult); - desktopBiometricsService.getBiometricsStatus.mockResolvedValue( - BiometricsStatus.ManualSetupNeeded, - ); - - await component.updateBiometricHandler(true); - - expect(biometricStateService.setBiometricUnlockEnabled).not.toHaveBeenCalled(); - expect(keyService.refreshAdditionalKeys).not.toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - - if (dialogResult) { - expect(platformUtilsService.launchUri).toHaveBeenCalledWith( - "https://bitwarden.com/help/biometrics/", - ); - } else { - expect(platformUtilsService.launchUri).not.toHaveBeenCalled(); - } - }, - ); - - it("sets up biometrics when auto setup is needed", async () => { - desktopBiometricsService.getBiometricsStatus.mockResolvedValue( - BiometricsStatus.AutoSetupNeeded, - ); - desktopBiometricsService.getBiometricsStatusForUser.mockResolvedValue( - BiometricsStatus.Available, - ); - - await component.updateBiometricHandler(true); - - expect(desktopBiometricsService.setupBiometrics).toHaveBeenCalled(); - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith( - true, - mockUserId, - ); - expect(component.form.controls.biometric.value).toBe(true); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - describe("windows test cases", () => { - beforeEach(() => { - platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - keyService.userKey$.mockReturnValue(of(mockUserKey)); - component.isWindows = true; - component.isLinux = false; - - desktopBiometricsService.getBiometricsStatus.mockResolvedValue( - BiometricsStatus.Available, - ); - desktopBiometricsService.getBiometricsStatusForUser.mockResolvedValue( - BiometricsStatus.Available, - ); - }); - - it("handles windows case", async () => { - await component.updateBiometricHandler(true); - - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith( - true, - mockUserId, - ); - expect(component.form.controls.autoPromptBiometrics.value).toBe(false); - expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith( - false, - mockUserId, - ); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId); - expect(component.form.controls.biometric.value).toBe(true); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - describe("when windows v2 biometrics is enabled", () => { - beforeEach(() => { - keyService.userKey$.mockReturnValue(of(mockUserKey)); - }); - - it("when the user doesn't have a master password or a PIN set, allows biometric unlock on app restart", async () => { - component.userHasMasterPassword = false; - component.userHasPinSet = false; - desktopBiometricsService.hasPersistentKey.mockResolvedValue(false); - - await component.updateBiometricHandler(true); - - expect(keyService.userKey$).toHaveBeenCalledWith(mockUserId); - expect(desktopBiometricsService.enrollPersistent).toHaveBeenCalledWith( - mockUserId, - mockUserKey, - ); - expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(false); - - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith( - true, - mockUserId, - ); - expect(component.form.controls.autoPromptBiometrics.value).toBe(false); - expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith( - false, - mockUserId, - ); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId); - expect(component.form.controls.biometric.value).toBe(true); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - test.each([ - [true, true], - [true, false], - [false, true], - ])( - "when the userHasMasterPassword is %s and userHasPinSet is %s, require master password/PIN on app restart is the default setting", - async (userHasMasterPassword, userHasPinSet) => { - component.userHasMasterPassword = userHasMasterPassword; - component.userHasPinSet = userHasPinSet; - - await component.updateBiometricHandler(true); - - expect(desktopBiometricsService.enrollPersistent).not.toHaveBeenCalled(); - expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(true); - expect(desktopBiometricsService.deleteBiometricUnlockKeyForUser).toHaveBeenCalledWith( - mockUserId, - ); - expect( - desktopBiometricsService.setBiometricProtectedUnlockKeyForUser, - ).toHaveBeenCalledWith(mockUserId, mockUserKey); - - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith( - true, - mockUserId, - ); - expect(component.form.controls.autoPromptBiometrics.value).toBe(false); - expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith( - false, - mockUserId, - ); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId); - expect(component.form.controls.biometric.value).toBe(true); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }, - ); - }); - }); - - it("handles linux case", async () => { - desktopBiometricsService.getBiometricsStatus.mockResolvedValue(BiometricsStatus.Available); - desktopBiometricsService.getBiometricsStatusForUser.mockResolvedValue( - BiometricsStatus.Available, - ); - - component.isWindows = false; - component.isLinux = true; - await component.updateBiometricHandler(true); - - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith( - true, - mockUserId, - ); - expect(component.form.controls.autoPromptBiometrics.value).toBe(false); - expect(biometricStateService.setPromptAutomatically).toHaveBeenCalledWith( - false, - mockUserId, - ); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId); - expect(component.form.controls.biometric.value).toBe(true); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - - test.each([ - BiometricsStatus.UnlockNeeded, - BiometricsStatus.HardwareUnavailable, - BiometricsStatus.AutoSetupNeeded, - BiometricsStatus.ManualSetupNeeded, - BiometricsStatus.PlatformUnsupported, - BiometricsStatus.DesktopDisconnected, - BiometricsStatus.NotEnabledLocally, - BiometricsStatus.NotEnabledInConnectedDesktopApp, - BiometricsStatus.NativeMessagingPermissionMissing, - ])( - `disables biometric when biometrics status check for the user returns %s`, - async (status) => { - desktopBiometricsService.getBiometricsStatus.mockResolvedValue( - BiometricsStatus.Available, - ); - desktopBiometricsService.getBiometricsStatusForUser.mockResolvedValue(status); - - await component.updateBiometricHandler(true); - - expect(keyService.refreshAdditionalKeys).toHaveBeenCalledWith(mockUserId); - expect(component.form.controls.biometric.value).toBe(false); - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledWith( - true, - mockUserId, - ); - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenCalledTimes(2); - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenLastCalledWith( - false, - mockUserId, - ); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }, - ); - }); - - describe("when updating to false", () => { - it("calls services to clear biometrics", async () => { - await component.ngOnInit(); - await component.updateBiometricHandler(false); - - expect(component.form.controls.biometric.value).toBe(false); - expect(biometricStateService.setBiometricUnlockEnabled).toHaveBeenLastCalledWith( - false, - mockUserId, - ); - expect(keyService.refreshAdditionalKeys).toHaveBeenCalled(); - expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); - }); - }); - }); - - describe("updateRequireMasterPasswordOnAppRestartHandler", () => { - beforeEach(() => { - jest.clearAllMocks(); - - keyService.userKey$.mockReturnValue(of(mockUserKey)); - }); - - test.each([true, false])(`handles thrown errors when updated to %s`, async (update) => { - const error = new Error("Test error"); - jest.spyOn(component, "updateRequireMasterPasswordOnAppRestart").mockRejectedValue(error); - - await component.ngOnInit(); - await component.updateRequireMasterPasswordOnAppRestartHandler(update, mockUserId); - - expect(logService.error).toHaveBeenCalled(); - expect(validationService.showError).toHaveBeenCalledWith(error); - }); - - describe("when updating to true", () => { - it("calls the biometrics service to clear and reset biometric key", async () => { - await component.ngOnInit(); - await component.updateRequireMasterPasswordOnAppRestartHandler(true, mockUserId); - - expect(keyService.userKey$).toHaveBeenCalledWith(mockUserId); - expect(desktopBiometricsService.deleteBiometricUnlockKeyForUser).toHaveBeenCalledWith( - mockUserId, - ); - expect(desktopBiometricsService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( - mockUserId, - mockUserKey, - ); - }); - }); - - describe("when updating to false", () => { - it("doesn't enroll persistent biometric if already enrolled", async () => { - desktopBiometricsService.hasPersistentKey.mockResolvedValue(false); - - await component.ngOnInit(); - await component.updateRequireMasterPasswordOnAppRestartHandler(false, mockUserId); - - expect(keyService.userKey$).toHaveBeenCalledWith(mockUserId); - expect(desktopBiometricsService.enrollPersistent).toHaveBeenCalledWith( - mockUserId, - mockUserKey, - ); - expect(component.form.controls.requireMasterPasswordOnAppRestart.value).toBe(false); - }); - }); - }); - - describe("desktop autotype", () => { - it("autotype should be hidden on mac os", async () => { - // Set OS - platformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop); - - // Recreate component to apply the correct device - fixture = TestBed.createComponent(SettingsComponent); - component = fixture.componentInstance; - - await component.ngOnInit(); - fixture.detectChanges(); - - // `enableAutotype` label shouldn't be found - const showEnableAutotypeLabelElement = fixture.debugElement.query( - By.css("label[for='enableAutotype']"), - ); - expect(showEnableAutotypeLabelElement).toBeNull(); - - // `showEnableAutotype` should be false - expect(component.showEnableAutotype).toBe(false); - }); - }); -}); diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts deleted file mode 100644 index 8dc0dfbce9ee..000000000000 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ /dev/null @@ -1,695 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { toSignal } from "@angular/core/rxjs-interop"; -import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { RouterModule } from "@angular/router"; -import { BehaviorSubject, Observable, Subject, firstValueFrom, of } from "rxjs"; -import { concatMap, map, switchMap, takeUntil, timeout } from "rxjs/operators"; - -import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { ClearClipboardDelay } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { DeviceType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; -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 { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; -import { - CheckboxModule, - DialogService, - FormFieldModule, - IconButtonModule, - IconModule, - ItemModule, - LinkModule, - SectionComponent, - SectionHeaderComponent, - SelectModule, - TypographyModule, -} from "@bitwarden/components"; -import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; -import { SessionTimeoutSettingsComponent } from "@bitwarden/key-management-ui"; -import { I18nPipe } from "@bitwarden/ui-common"; -import { - PermitCipherDetailsPopoverComponent, - VaultCopyButtonsService, - ShowQuickCopyActionsDetailsPopoverComponent, -} from "@bitwarden/vault"; - -import { SetPinComponent } from "../../auth/components/set-pin.component"; -import { AutotypeShortcutComponent } from "../../autofill/components/autotype-shortcut.component"; -import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; -import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; -import { DesktopAutotypeMvpService } from "../../autofill/services/desktop-autotype-mvp.service"; -import { DesktopPremiumUpgradePromptService } from "../../billing/services/desktop-premium-upgrade-prompt.service"; -import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service"; -import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; -import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; - -/** - * @deprecated - In process of being migrated to `settings-dialog.component.ts`. Ensure both are updated. - */ -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - selector: "app-settings", - templateUrl: "settings.component.html", - standalone: true, - providers: [ - { - provide: PremiumUpgradePromptService, - useClass: DesktopPremiumUpgradePromptService, - }, - ], - imports: [ - CheckboxModule, - CommonModule, - FormFieldModule, - FormsModule, - ReactiveFormsModule, - IconButtonModule, - IconModule, - ItemModule, - I18nPipe, - LinkModule, - RouterModule, - SectionComponent, - SectionHeaderComponent, - SelectModule, - TypographyModule, - SessionTimeoutSettingsComponent, - PermitCipherDetailsPopoverComponent, - PremiumBadgeComponent, - ShowQuickCopyActionsDetailsPopoverComponent, - ], -}) -export class SettingsComponent implements OnInit, OnDestroy { - localeOptions: any[]; - themeOptions: any[]; - clearClipboardOptions: any[]; - sshAgentPromptBehaviorOptions: any[]; - supportsBiometric: boolean; - private timerId: any; - showDuckDuckGoIntegrationOption = false; - showEnableAutotype = false; - autotypeShortcut: string; - showOpenAtLoginOption = false; - isWindows: boolean; - isLinux: boolean; - isMac: boolean; - - runInBackgroundText: string; - runInBackgroundDescText: string; - - showSecurity = true; - showAccountPreferences = true; - showAppPreferences = true; - - currentUserEmail: string; - currentUserId: UserId; - - userHasMasterPassword: boolean; - userHasPinSet: boolean; - - pinEnabled$: Observable = of(true); - - /** Controls whether the quick copy actions setting is shown */ - protected readonly showQuickCopyActionsSetting = toSignal( - this.configService.getFeatureFlag$(FeatureFlag.PM40435_QuickCopyIconSetting), - { initialValue: false }, - ); - - form = this.formBuilder.group({ - // Security - pin: [null as boolean | null], - biometric: false, - requireMasterPasswordOnAppRestart: true, - autoPromptBiometrics: false, - // Account Preferences - clearClipboard: [null], - minimizeOnCopyToClipboard: false, - enableFavicons: false, - // App Settings - runInBackground: false, - openAtLogin: false, - enableHardwareAcceleration: true, - enableSshAgent: false, - sshAgentPromptBehavior: SshAgentPromptType.Always, - allowScreenshots: false, - enableDuckDuckGoBrowserIntegration: false, - showQuickCopyActions: false, - enableAutotype: this.formBuilder.control({ - value: false, - disabled: true, - }), - autotypeShortcut: [null as string | null], - theme: [null as Theme | null], - locale: [null as string | null], - }); - - protected refreshTimeoutSettings$ = new BehaviorSubject(undefined); - private destroy$ = new Subject(); - - constructor( - private accountService: AccountService, - private policyService: PolicyService, - private formBuilder: FormBuilder, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private vaultTimeoutSettingsService: VaultTimeoutSettingsService, - private stateService: StateService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private messagingService: MessagingService, - private keyService: KeyService, - private themeStateService: ThemeStateService, - private domainSettingsService: DomainSettingsService, - private dialogService: DialogService, - private userVerificationService: UserVerificationServiceAbstraction, - private desktopSettingsService: DesktopSettingsService, - private desktopAutotypeMvpService: DesktopAutotypeMvpService, - private biometricStateService: BiometricStateService, - private biometricsService: DesktopBiometricsService, - private desktopAutofillSettingsService: DesktopAutofillSettingsService, - private pinService: PinServiceAbstraction, - private logService: LogService, - private nativeMessagingManifestService: NativeMessagingManifestService, - private configService: ConfigService, - private validationService: ValidationService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - private vaultCopyButtonsService: VaultCopyButtonsService, - ) { - this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; - this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop; - this.isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop; - - this.runInBackgroundText = this.i18nService.t("runInBackground"); - this.runInBackgroundDescText = this.i18nService.t( - this.isMac ? "runInBackgroundDescMac" : "runInBackgroundDesc", - ); - - this.showOpenAtLoginOption = this.showAutostartSetting(); - - // DuckDuckGo browser is only for macos initially - this.showDuckDuckGoIntegrationOption = this.isMac; - - const localeOptions: any[] = []; - this.i18nService.supportedTranslationLocales.forEach((locale) => { - let name = locale; - if (this.i18nService.localeNames.has(locale)) { - name += " - " + this.i18nService.localeNames.get(locale); - } - localeOptions.push({ name: name, value: locale }); - }); - localeOptions.sort(Utils.getSortFunction(this.i18nService, "name")); - localeOptions.splice(0, 0, { name: this.i18nService.t("default"), value: null }); - this.localeOptions = localeOptions; - - this.themeOptions = [ - { name: this.i18nService.t("default"), value: ThemeTypes.System }, - { name: this.i18nService.t("light"), value: ThemeTypes.Light }, - { name: this.i18nService.t("dark"), value: ThemeTypes.Dark }, - ]; - - this.clearClipboardOptions = [ - { name: i18nService.t("never"), value: ClearClipboardDelay.Never }, - { name: i18nService.t("tenSeconds"), value: ClearClipboardDelay.TenSeconds }, - { name: i18nService.t("twentySeconds"), value: ClearClipboardDelay.TwentySeconds }, - { name: i18nService.t("thirtySeconds"), value: ClearClipboardDelay.ThirtySeconds }, - { name: i18nService.t("oneMinute"), value: ClearClipboardDelay.OneMinute }, - { name: i18nService.t("twoMinutes"), value: ClearClipboardDelay.TwoMinutes }, - { name: i18nService.t("fiveMinutes"), value: ClearClipboardDelay.FiveMinutes }, - ]; - this.sshAgentPromptBehaviorOptions = [ - { - name: this.i18nService.t("sshAgentPromptBehaviorAlways"), - value: SshAgentPromptType.Always, - }, - { name: this.i18nService.t("sshAgentPromptBehaviorNever"), value: SshAgentPromptType.Never }, - { - name: this.i18nService.t("sshAgentPromptBehaviorRememberUntilLock"), - value: SshAgentPromptType.RememberUntilLock, - }, - ]; - } - - async ngOnInit() { - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - - // Autotype is for Windows initially - const isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop; - if (isWindows) { - this.configService - .getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype) - .pipe(takeUntil(this.destroy$)) - .subscribe((enabled) => { - this.showEnableAutotype = enabled; - }); - } - - this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword(); - - this.currentUserEmail = activeAccount.email; - this.currentUserId = activeAccount.id; - - // Load initial values - this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id); - - this.pinEnabled$ = this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => - this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId), - ), - getFirstPolicy, - map((policy) => { - return policy == null || !policy.enabled; - }), - ); - - const initialValues = { - pin: this.userHasPinSet, - biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(activeAccount.id), - requireMasterPasswordOnAppRestart: !(await this.biometricsService.hasPersistentKey( - activeAccount.id, - )), - autoPromptBiometrics: await firstValueFrom( - this.biometricStateService.promptAutomatically$(activeAccount.id), - ), - clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$), - minimizeOnCopyToClipboard: await firstValueFrom(this.desktopSettingsService.minimizeOnCopy$), - enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), - runInBackground: await firstValueFrom(this.desktopSettingsService.runInBackground$), - openAtLogin: await firstValueFrom(this.desktopSettingsService.openAtLogin$), - enableDuckDuckGoBrowserIntegration: await firstValueFrom( - this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, - ), - showQuickCopyActions: await firstValueFrom( - this.vaultCopyButtonsService.showQuickCopyActions$, - ), - enableHardwareAcceleration: await firstValueFrom( - this.desktopSettingsService.hardwareAcceleration$, - ), - enableSshAgent: await firstValueFrom(this.desktopSettingsService.sshAgentEnabled$), - sshAgentPromptBehavior: await firstValueFrom( - this.desktopSettingsService.sshAgentPromptBehavior$, - ), - allowScreenshots: !(await firstValueFrom(this.desktopSettingsService.preventScreenshots$)), - enableAutotype: await firstValueFrom( - this.desktopAutotypeMvpService.autotypeEnabledUserSetting$, - ), - autotypeShortcut: this.getFormattedAutotypeShortcutText( - (await firstValueFrom(this.desktopAutotypeMvpService.autotypeKeyboardShortcut$)) ?? [], - ), - theme: await firstValueFrom(this.themeStateService.selectedTheme$), - locale: await firstValueFrom(this.i18nService.userSetLocale$), - }; - this.form.setValue(initialValues, { emitEvent: false }); - - if (isWindows) { - this.billingAccountProfileStateService - .hasPremiumFromAnySource$(activeAccount.id) - .pipe(takeUntil(this.destroy$)) - .subscribe((hasPremium) => { - if (hasPremium) { - this.form.controls.enableAutotype.enable(); - } - }); - } - - // Form events - this.form.controls.pin.valueChanges - .pipe( - concatMap(async (value) => { - await this.updatePinHandler(value); - this.refreshTimeoutSettings$.next(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.form.controls.biometric.valueChanges - .pipe( - concatMap(async (enabled) => { - await this.updateBiometricHandler(enabled); - this.refreshTimeoutSettings$.next(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.form.controls.requireMasterPasswordOnAppRestart.valueChanges - .pipe( - concatMap(async (value) => { - await this.updateRequireMasterPasswordOnAppRestartHandler(value, activeAccount.id); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.supportsBiometric = await this.biometricsService.canEnableBiometricUnlock(); - this.timerId = setInterval(async () => { - this.supportsBiometric = await this.biometricsService.canEnableBiometricUnlock(); - }, 1000); - } - - async updatePinHandler(value: boolean) { - try { - await this.updatePin(value); - } catch (error) { - this.logService.error("Error updating unlock with PIN: ", error); - this.form.controls.pin.setValue(!value, { emitEvent: false }); - this.validationService.showError(error); - } finally { - this.messagingService.send("redrawMenu"); - } - } - - async updatePin(value: boolean) { - if (value) { - const dialogRef = SetPinComponent.open(this.dialogService); - - if (dialogRef == null) { - this.form.controls.pin.setValue(false, { emitEvent: false }); - return; - } - - this.userHasPinSet = await firstValueFrom(dialogRef.closed); - this.form.controls.pin.setValue(this.userHasPinSet, { emitEvent: false }); - } else { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - // On Windows if a user turned off PIN without having a MP and has biometrics + require MP/PIN on restart enabled. - if ( - this.isWindows && - this.supportsBiometric && - this.form.value.requireMasterPasswordOnAppRestart && - this.form.value.biometric && - !this.userHasMasterPassword - ) { - // Allow biometric unlock on app restart so the user doesn't get into a bad state. - await this.enrollPersistentBiometricIfNeeded(userId); - } - await this.pinService.unsetPin(userId); - } - } - - async updateBiometricHandler(value: boolean) { - try { - await this.updateBiometric(value); - } catch (error) { - this.logService.error("Error updating unlock with biometrics: ", error); - this.form.controls.biometric.setValue(false, { emitEvent: false }); - this.validationService.showError(error); - } finally { - this.messagingService.send("redrawMenu"); - } - } - - async updateBiometric(enabled: boolean) { - // NOTE: A bug in angular causes [ngModel] to not reflect the backing field value - // causing the checkbox to remain checked even if authentication fails. - // The bug should resolve itself once the angular issue is resolved. - // See: https://github.com/angular/angular/issues/13063 - - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - if (!enabled || !this.supportsBiometric) { - this.form.controls.biometric.setValue(false, { emitEvent: false }); - await this.biometricStateService.setBiometricUnlockEnabled(false, activeUserId); - await this.keyService.refreshAdditionalKeys(activeUserId); - return; - } - - const status = await this.biometricsService.getBiometricsStatus(); - - if (status === BiometricsStatus.AutoSetupNeeded) { - await this.biometricsService.setupBiometrics(); - } else if (status === BiometricsStatus.ManualSetupNeeded) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "biometricsManualSetupTitle" }, - content: { key: "biometricsManualSetupDesc" }, - type: "warning", - }); - if (confirmed) { - this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/"); - } - return; - } - - await this.biometricStateService.setBiometricUnlockEnabled(true, activeUserId); - if (this.isWindows) { - // Recommended settings for Windows Hello - this.form.controls.autoPromptBiometrics.setValue(false); - await this.biometricStateService.setPromptAutomatically(false, activeUserId); - - // If the user doesn't have a MP or PIN then they have to use biometrics on app restart. - if (!this.userHasMasterPassword && !this.userHasPinSet) { - // Allow biometric unlock on app restart so the user doesn't get into a bad state. - await this.enrollPersistentBiometricIfNeeded(activeUserId); - } else { - this.form.controls.requireMasterPasswordOnAppRestart.setValue(true); - } - } else if (this.isLinux) { - // Similar to Windows - this.form.controls.autoPromptBiometrics.setValue(false); - await this.biometricStateService.setPromptAutomatically(false, activeUserId); - } - await this.keyService.refreshAdditionalKeys(activeUserId); - - // Validate the key is stored in case biometrics fail. - const biometricSet = - (await this.biometricsService.getBiometricsStatusForUser(activeUserId)) === - BiometricsStatus.Available; - this.form.controls.biometric.setValue(biometricSet, { emitEvent: false }); - if (!biometricSet) { - await this.biometricStateService.setBiometricUnlockEnabled(false, activeUserId); - } - } - - async updateRequireMasterPasswordOnAppRestartHandler(enabled: boolean, userId: UserId) { - try { - await this.updateRequireMasterPasswordOnAppRestart(enabled, userId); - } catch (error) { - this.logService.error("Error updating require master password on app restart: ", error); - this.validationService.showError(error); - } - } - - async updateRequireMasterPasswordOnAppRestart(enabled: boolean, userId: UserId) { - if (enabled) { - // Require master password or PIN on app restart - const userKey = await firstValueFrom(this.keyService.userKey$(userId)); - await this.biometricsService.deleteBiometricUnlockKeyForUser(userId); - await this.biometricsService.setBiometricProtectedUnlockKeyForUser(userId, userKey); - } else { - // Allow biometric unlock on app restart - await this.enrollPersistentBiometricIfNeeded(userId); - } - } - - private async enrollPersistentBiometricIfNeeded(userId: UserId): Promise { - if (!(await this.biometricsService.hasPersistentKey(userId))) { - const userKey = await firstValueFrom(this.keyService.userKey$(userId)); - await this.biometricsService.enrollPersistent(userId, userKey); - this.form.controls.requireMasterPasswordOnAppRestart.setValue(false, { - emitEvent: false, - }); - } - } - - async updateAutoPromptBiometrics() { - if (this.form.value.autoPromptBiometrics) { - await this.biometricStateService.setPromptAutomatically(true, this.currentUserId); - } else { - await this.biometricStateService.setPromptAutomatically(false, this.currentUserId); - } - } - - async saveFavicons() { - await this.domainSettingsService.setShowFavicons(this.form.value.enableFavicons); - this.messagingService.send("refreshCiphers"); - } - - async saveQuickCopyActions() { - await this.vaultCopyButtonsService.setShowQuickCopyActions( - this.form.value.showQuickCopyActions, - ); - } - - async saveRunInBackground() { - await this.desktopSettingsService.setRunInBackground(this.form.value.runInBackground); - } - - async saveLocale() { - await this.i18nService.setLocale(this.form.value.locale); - } - - async saveTheme() { - await this.themeStateService.setSelectedTheme(this.form.value.theme); - } - - async saveMinOnCopyToClipboard() { - await this.desktopSettingsService.setMinimizeOnCopy( - this.form.value.minimizeOnCopyToClipboard, - this.currentUserId, - ); - } - - async saveClearClipboard() { - await this.autofillSettingsService.setClearClipboardDelay(this.form.value.clearClipboard); - } - - private showAutostartSetting(): boolean { - // Windows store does not support autostart. - // Dev mode should not show auto-start, because it would result in an empty electron window starting on login. - return !ipc.platform.isWindowsStore && !ipc.platform.isDev; - } - - async saveOpenAtLogin() { - await this.desktopSettingsService.setOpenAtLogin(this.form.value.openAtLogin); - // TODO: Ideally DesktopSettingsService.openAtLogin$ could be subscribed to directly rather than sending a message - this.messagingService.send( - this.form.value.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin", - ); - } - - async saveDdgBrowserIntegration() { - await this.desktopAutofillSettingsService.setEnableDuckDuckGoBrowserIntegration( - this.form.value.enableDuckDuckGoBrowserIntegration, - ); - - // Adding to cover users on a previous version of DDG - await this.stateService.setEnableDuckDuckGoBrowserIntegration( - this.form.value.enableDuckDuckGoBrowserIntegration, - ); - - const errorResult = await this.nativeMessagingManifestService.generateDuckDuckGo( - this.form.value.enableDuckDuckGoBrowserIntegration, - ); - if (errorResult !== null) { - this.logService.error("Error in DDG browser integration: " + errorResult); - } - } - - async saveHardwareAcceleration() { - await this.desktopSettingsService.setHardwareAcceleration( - this.form.value.enableHardwareAcceleration, - ); - } - - async saveSshAgent() { - await this.desktopSettingsService.setSshAgentEnabled(this.form.value.enableSshAgent); - } - - async saveSshAgentPromptBehavior() { - await this.desktopSettingsService.setSshAgentPromptBehavior( - this.form.value.sshAgentPromptBehavior, - ); - } - - async savePreventScreenshots() { - await this.desktopSettingsService.setPreventScreenshots(!this.form.value.allowScreenshots); - - if (!this.form.value.allowScreenshots) { - const dialogRef = this.dialogService.openSimpleDialogRef({ - title: { key: "confirmWindowStillVisibleTitle" }, - content: { key: "confirmWindowStillVisibleContent" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "info", - }); - let enabled = true; - try { - enabled = await firstValueFrom(dialogRef.closed.pipe(timeout(10000))); - } catch { - enabled = false; - } finally { - await dialogRef.close(); - } - - if (!enabled) { - await this.desktopSettingsService.setPreventScreenshots(false); - this.form.controls.allowScreenshots.setValue(true, { emitEvent: false }); - } - } - } - - async saveEnableAutotype() { - await this.desktopAutotypeMvpService.setAutotypeEnabledState(this.form.value.enableAutotype); - const currentShortcut = await firstValueFrom( - this.desktopAutotypeMvpService.autotypeKeyboardShortcut$, - ); - if (currentShortcut) { - this.form.controls.autotypeShortcut.setValue( - this.getFormattedAutotypeShortcutText(currentShortcut), - ); - } - } - - async saveAutotypeShortcut() { - // disable the shortcut so that the user can't re-enter the existing - // shortcut and trigger the feature during the settings menu. - // it is not necessary to check if it's already enabled, because - // the edit shortcut is only available if the feature is enabled - // in the settings. - await this.desktopAutotypeMvpService.setAutotypeEnabledState(false); - - const dialogRef = AutotypeShortcutComponent.open(this.dialogService); - - const newShortcutArray = await firstValueFrom(dialogRef.closed); - - // re-enable - await this.desktopAutotypeMvpService.setAutotypeEnabledState(true); - - if (!newShortcutArray) { - return; - } - - this.form.controls.autotypeShortcut.setValue( - this.getFormattedAutotypeShortcutText(newShortcutArray), - ); - await this.desktopAutotypeMvpService.setAutotypeKeyboardShortcutState(newShortcutArray); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - clearInterval(this.timerId); - } - - get biometricText() { - switch (this.platformUtilsService.getDevice()) { - case DeviceType.MacOsDesktop: - return "unlockWithTouchId"; - case DeviceType.WindowsDesktop: - return "unlockWithWindowsHello"; - case DeviceType.LinuxDesktop: - return "unlockWithPolkit"; - default: - throw new Error("Unsupported platform"); - } - } - - getFormattedAutotypeShortcutText(shortcut: string[]) { - return shortcut ? shortcut.join("+").replace("Super", "Win") : null; - } -} From 0f8d85c2168daa4305d00e6fdef7738c89d0a5f3 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Thu, 13 Aug 2026 17:02:10 +0200 Subject: [PATCH 3/5] Remove feature-flag definition of "desktop-ui-settings-dialog" --- libs/common/src/enums/feature-flag.enum.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 4b048618c692..0973a76816d0 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -122,9 +122,6 @@ export enum FeatureFlag { /* Innovation */ ElectronStorageCache = "pm-32783-electron-storage-cache", - /* Desktop */ - DesktopSettingsDialog = "desktop-ui-settings-dialog", - /* PAM */ Pam = "pm-37044-pam-v-0", @@ -254,9 +251,6 @@ export const DefaultFeatureFlagValue = { /* Innovation */ [FeatureFlag.ElectronStorageCache]: FALSE, - /* Desktop */ - [FeatureFlag.DesktopSettingsDialog]: FALSE, - /* PAM */ [FeatureFlag.Pam]: FALSE, From b74c9f0182f32432224a55f5e94eda76b25f448b Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Thu, 13 Aug 2026 17:13:03 +0200 Subject: [PATCH 4/5] Remove unused template --- apps/desktop/src/app/app.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 6a90e87353eb..4ed58d61d269 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -95,7 +95,6 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours selector: "app-root", styles: [], template: ` - @if (showHeader$ | async) { From 1e1b77516ebef95ffadcc778b9026a03df096140 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Tue, 18 Aug 2026 13:14:56 +0200 Subject: [PATCH 5/5] Remove unused method to open modals. Modals should be replaced by dialogs --- apps/desktop/src/app/app.component.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 3415c3bd45bc..051b84c1e5a7 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -1,14 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - Component, - DestroyRef, - NgZone, - OnDestroy, - OnInit, - Type, - ViewContainerRef, -} from "@angular/core"; +import { Component, DestroyRef, NgZone, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; import { filter, firstValueFrom, lastValueFrom, map, Subject, takeUntil, timeout } from "rxjs"; @@ -16,7 +8,6 @@ import { filter, firstValueFrom, lastValueFrom, map, Subject, takeUntil, timeout import { AccountDeletionService } from "@bitwarden/angular/auth/account-deletion/account-deletion.service"; import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; -import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; @@ -116,7 +107,6 @@ export class AppComponent implements OnInit, OnDestroy { loading = false; private lastActivity: Date = null; - private modal: ModalRef = null; private idleTimer: number = null; private isIdle = false; private activeUserId: UserId = null; @@ -823,17 +813,6 @@ export class AppComponent implements OnInit, OnDestroy { } } - private async openModal(type: Type, ref: ViewContainerRef) { - this.modalService.closeAll(); - - [this.modal] = await this.modalService.openViewRef(type, ref); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.modal.onClosed.subscribe(() => { - this.modal = null; - }); - } - private routeToVault(action: string, cipherType: CipherType) { if (!this.router.url.includes("vault")) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.