1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 10:54:00 +00:00
Files
browser/libs/key-management-ui/src/lock/components/lock.component.ts
Bernd Schoolmann 31eb984477 [PM-31012] Improve loading time for lock component (#18450)
* Improve loading time for lock component

* Reset interval to 1000

* Remove interval import
2026-02-17 09:39:28 -08:00

808 lines
27 KiB
TypeScript

import { CommonModule } from "@angular/common";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Router, ActivatedRoute } from "@angular/router";
import {
BehaviorSubject,
filter,
firstValueFrom,
timer,
mergeMap,
Subject,
switchMap,
takeUntil,
tap,
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LogoutService } from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { SyncService } from "@bitwarden/common/platform/sync";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserKey } from "@bitwarden/common/types/key";
import {
TooltipDirective,
AsyncActionsModule,
AnonLayoutWrapperDataService,
ButtonModule,
DialogService,
FormFieldModule,
IconButtonModule,
ToastService,
} from "@bitwarden/components";
import {
KeyService,
BiometricStateService,
BiometricsService,
BiometricsStatus,
UserAsymmetricKeysRegenerationService,
} from "@bitwarden/key-management";
import {
UnlockOption,
LockComponentService,
UnlockOptions,
UnlockOptionValue,
} from "../services/lock-component.service";
import { MasterPasswordLockComponent } from "./master-password-lock/master-password-lock.component";
import { UnlockViaPrfComponent } from "./unlock-via-prf.component";
const BroadcasterSubscriptionId = "LockComponent";
const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
[ClientType.Web]: "vault",
[ClientType.Desktop]: "vault",
[ClientType.Browser]: "/tabs/current",
};
type AfterUnlockActions = {
passwordEvaluation?: {
masterPassword: string;
};
};
/// The minimum amount of time to wait after a process reload for a biometrics auto prompt to be possible
/// Fixes safari autoprompt behavior
const AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY = 5000;
const BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES = [
BiometricsStatus.HardwareUnavailable,
BiometricsStatus.DesktopDisconnected,
BiometricsStatus.NotEnabledInConnectedDesktopApp,
];
// 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: "bit-lock",
templateUrl: "lock.component.html",
imports: [
CommonModule,
JslibModule,
ReactiveFormsModule,
ButtonModule,
FormFieldModule,
AsyncActionsModule,
IconButtonModule,
UnlockViaPrfComponent,
MasterPasswordLockComponent,
TooltipDirective,
],
})
export class LockComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected loading = true;
activeAccount: Account | null = null;
clientType?: ClientType;
unlockOptions: UnlockOptions | null = null;
UnlockOption = UnlockOption;
private _activeUnlockOptionBSubject: BehaviorSubject<UnlockOptionValue | null> =
new BehaviorSubject<UnlockOptionValue | null>(null);
activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable();
set activeUnlockOption(value: UnlockOptionValue | null) {
this._activeUnlockOptionBSubject.next(value);
}
get activeUnlockOption(): UnlockOptionValue | null {
return this._activeUnlockOptionBSubject.value;
}
private invalidPinAttempts = 0;
biometricUnlockBtnText?: string;
showPassword = false;
private enforcedMasterPasswordOptions?: MasterPasswordPolicyOptions = undefined;
formGroup: FormGroup | null = null;
// Browser extension properties:
shouldClosePopout = false;
// Desktop properties:
private deferFocus: boolean | null = null;
private biometricAsked = false;
defaultUnlockOptionSetForUser = false;
unlockingViaBiometrics = false;
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private keyService: KeyService,
private platformUtilsService: PlatformUtilsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private dialogService: DialogService,
private messagingService: MessagingService,
private biometricStateService: BiometricStateService,
private ngZone: NgZone,
private i18nService: I18nService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private logService: LogService,
private deviceTrustService: DeviceTrustServiceAbstraction,
private syncService: SyncService,
private policyService: InternalPolicyService,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
private biometricService: BiometricsService,
private logoutService: LogoutService,
private lockComponentService: LockComponentService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private encryptedMigrator: EncryptedMigrator,
// desktop deps
private broadcasterService: BroadcasterService,
) {}
async ngOnInit() {
this.listenForActiveUnlockOptionChanges();
// Listen for active account changes
this.listenForActiveAccountChanges();
this.listenForUnlockOptionsChanges();
// Identify client
this.clientType = this.platformUtilsService.getClientType();
if (this.clientType === ClientType.Desktop) {
await this.desktopOnInit();
} else if (this.clientType === ClientType.Browser) {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
}
}
private listenForUnlockOptionsChanges() {
timer(0, 1000)
.pipe(
mergeMap(async () => {
if (this.activeAccount?.id != null) {
const prevBiometricsEnabled = this.unlockOptions?.biometrics.enabled;
this.unlockOptions = await firstValueFrom(
this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id),
);
if (this.activeUnlockOption == null) {
this.loading = false;
await this.setDefaultActiveUnlockOption(this.unlockOptions);
} else if (!prevBiometricsEnabled && this.unlockOptions?.biometrics.enabled) {
await this.setDefaultActiveUnlockOption(this.unlockOptions);
if (this.activeUnlockOption === UnlockOption.Biometrics) {
await this.handleBiometricsUnlockEnabled();
}
}
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
// Base component methods
private listenForActiveUnlockOptionChanges() {
this.activeUnlockOption$
.pipe(takeUntil(this.destroy$))
.subscribe((activeUnlockOption: UnlockOptionValue | null) => {
if (activeUnlockOption === UnlockOption.Pin) {
this.buildPinForm();
}
});
}
private buildPinForm() {
this.formGroup = this.formBuilder.group(
{
pin: ["", [Validators.required]],
},
{ updateOn: "submit" },
);
}
private listenForActiveAccountChanges() {
this.accountService.activeAccount$
.pipe(
tap((account) => {
this.loading = true;
this.activeAccount = account;
this.resetDataOnActiveAccountChange();
}),
filter((account): account is Account => account != null),
switchMap(async (account) => {
await this.handleActiveAccountChange(account);
this.loading = false;
}),
takeUntil(this.destroy$),
)
.subscribe();
}
private async handleActiveAccountChange(activeAccount: Account) {
// this account may be unlocked, prevent any prompts so we can redirect to vault
if (await this.keyService.hasUserKey(activeAccount.id)) {
return;
}
this.setEmailAsPageSubtitle(activeAccount.email);
this.unlockOptions = await firstValueFrom(
this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id),
);
const canUseBiometrics = [
BiometricsStatus.Available,
...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES,
].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id));
if (
!this.unlockOptions?.masterPassword.enabled &&
!this.unlockOptions?.pin.enabled &&
!canUseBiometrics
) {
// User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set.
this.logService.warning("[LockComponent] User cannot unlock again. Logging out!");
await this.logoutService.logout(activeAccount.id);
return;
}
await this.setDefaultActiveUnlockOption(this.unlockOptions);
if (this.unlockOptions?.biometrics.enabled) {
await this.handleBiometricsUnlockEnabled();
}
}
private resetDataOnActiveAccountChange() {
this.defaultUnlockOptionSetForUser = false;
this.unlockOptions = null;
this.activeUnlockOption = null;
this.formGroup = null; // new form group will be created based on new active unlock option
// Desktop properties:
this.biometricAsked = false;
}
private setEmailAsPageSubtitle(email: string) {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageSubtitle: email,
});
}
private async setDefaultActiveUnlockOption(unlockOptions: UnlockOptions | null) {
// Priorities should be Biometrics > Pin > Master Password for speed
if (unlockOptions?.biometrics.enabled) {
this.activeUnlockOption = UnlockOption.Biometrics;
} else if (unlockOptions?.pin.enabled) {
this.activeUnlockOption = UnlockOption.Pin;
} else if (unlockOptions?.masterPassword.enabled) {
this.activeUnlockOption = UnlockOption.MasterPassword;
} else if (
unlockOptions != null &&
BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES.includes(
unlockOptions.biometrics.biometricsStatus,
)
) {
// If biometrics is temporarily unavailable for masterpassword-less users, but they have biometrics configured,
// then show the biometrics screen so the user knows why they can't unlock, and to give them the option to log out.
this.activeUnlockOption = UnlockOption.Biometrics;
}
}
private async handleBiometricsUnlockEnabled() {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
const autoPromptBiometrics = await firstValueFrom(
this.biometricStateService.promptAutomatically$,
);
// TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the
// desktop and extension.
if (this.clientType === "desktop") {
if (autoPromptBiometrics) {
this.loading = false;
await this.desktopAutoPromptBiometrics();
}
}
if (this.clientType === "browser") {
if (
this.unlockOptions?.biometrics.enabled &&
autoPromptBiometrics &&
(await this.biometricService.getShouldAutopromptNow())
) {
await this.biometricService.setShouldAutopromptNow(false);
const lastProcessReload = await this.biometricStateService.getLastProcessReload();
if (
lastProcessReload == null ||
isNaN(lastProcessReload.getTime()) ||
Date.now() - lastProcessReload.getTime() > AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY
) {
// Firefox extension closes the popup when unfocused during biometric unlock, pop out the window to prevent infinite loop.
if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) {
await this.lockComponentService.popOutBrowserExtension();
this.shouldClosePopout = true;
}
this.loading = false;
await this.unlockViaBiometrics();
}
}
}
}
// Note: this submit method is only used for unlock methods that require a form and user input.
// For biometrics unlock, the method is called directly.
submit = async (): Promise<void> => {
if (this.activeUnlockOption === UnlockOption.Pin) {
return await this.unlockViaPin();
}
};
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
acceptButtonText: { key: "logOut" },
type: "warning",
});
if (confirmed && this.activeAccount != null) {
await this.logoutService.logout(this.activeAccount.id);
// navigate to root so redirect guard can properly route next active user or null user to correct page
await this.router.navigate(["/"]);
}
}
async unlockViaBiometrics(): Promise<void> {
this.unlockingViaBiometrics = true;
if (
this.unlockOptions == null ||
!this.unlockOptions.biometrics.enabled ||
this.activeAccount == null
) {
this.unlockingViaBiometrics = false;
return;
}
try {
await this.biometricStateService.setUserPromptCancelled();
const userKey = await this.biometricService.unlockWithBiometricsForUser(
this.activeAccount.id,
);
// If user cancels biometric prompt, userKey is undefined.
if (userKey) {
await this.setUserKeyAndContinue(userKey);
}
this.unlockingViaBiometrics = false;
} catch (e) {
// Cancelling is a valid action.
if (e instanceof Error && e.message === "canceled") {
this.unlockingViaBiometrics = false;
return;
}
this.logService.error("[LockComponent] Failed to unlock via biometrics.", e);
let biometricTranslatedErrorDesc;
if (this.clientType === "browser") {
const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e);
if (biometricErrorDescTranslationKey) {
biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey);
}
}
// if no translation key found, show generic error message
if (!biometricTranslatedErrorDesc) {
biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError");
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "error" },
content: biometricTranslatedErrorDesc,
acceptButtonText: { key: "tryAgain" },
type: "danger",
});
if (confirmed) {
// try again
await this.unlockViaBiometrics();
}
this.unlockingViaBiometrics = false;
}
}
async onPrfUnlockSuccess(userKey: UserKey): Promise<void> {
await this.setUserKeyAndContinue(userKey);
}
togglePassword() {
this.showPassword = !this.showPassword;
}
private validatePin(): boolean {
if (this.formGroup?.invalid) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("pinRequired"),
});
return false;
}
return true;
}
private async unlockViaPin() {
if (!this.validatePin() || this.formGroup == null || this.activeAccount == null) {
return;
}
const pin = this.formGroup.controls.pin.value;
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
try {
const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id);
if (userKey) {
await this.setUserKeyAndContinue(userKey);
return; // successfully unlocked
}
// Failure state: invalid PIN or failed decryption
this.invalidPinAttempts++;
// Log user out if they have entered an invalid PIN too many times
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
});
this.messagingService.send("logout");
return;
}
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidPin"),
});
} catch {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("unexpectedError"),
});
}
}
async successfulMasterPasswordUnlock(event: {
userKey: UserKey;
masterPassword: string;
}): Promise<void> {
if (event.userKey == null || !event.masterPassword) {
this.logService.error(
"[LockComponent] successfulMasterPasswordUnlock called with invalid data.",
);
return;
}
await this.setUserKeyAndContinue(event.userKey, {
passwordEvaluation: {
masterPassword: event.masterPassword,
},
});
}
protected async setUserKeyAndContinue(
key: UserKey,
afterUnlockActions: AfterUnlockActions = {},
): Promise<void> {
if (this.activeAccount == null) {
throw new Error("No active user.");
}
// Add a mark to indicate that the user has unlocked their vault. A good starting point for measuring unlock performance.
this.logService.mark("Vault unlocked");
await this.keyService.setUserKey(key, this.activeAccount.id);
await this.pinService.userUnlocked(this.activeAccount.id);
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustService.trustDeviceIfRequired(this.activeAccount.id);
await this.doContinue(afterUnlockActions);
}
private async doContinue(afterUnlockActions: AfterUnlockActions) {
if (this.activeAccount == null) {
throw new Error("No active user.");
}
await this.biometricStateService.resetUserPromptCancelled();
try {
await this.encryptedMigrator.runMigrations(
this.activeAccount.id,
afterUnlockActions.passwordEvaluation?.masterPassword ?? null,
);
} catch {
// Don't block login success on migration failure
}
this.messagingService.send("unlocked");
if (afterUnlockActions.passwordEvaluation) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (userId == null) {
throw new Error("No active user.");
}
try {
// If we do not have any saved policies, attempt to load them from the service
if (this.enforcedMasterPasswordOptions == undefined) {
this.enforcedMasterPasswordOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
),
);
}
if (this.requirePasswordChange(afterUnlockActions.passwordEvaluation.masterPassword)) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
}
} catch (e) {
// Do not prevent unlock if there is an error evaluating policies
this.logService.error(e);
}
}
// Vault can be de-synced since server notifications get ignored while locked. Need to check whether sync is required using the sync service.
const startSync = new Date().getTime();
// TODO: This should probably not be blocking
await this.syncService.fullSync(false);
this.logService.info(`[LockComponent] Sync took ${new Date().getTime() - startSync}ms`);
const startRegeneration = new Date().getTime();
// TODO: This should probably not be blocking
await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(this.activeAccount.id);
this.logService.info(
`[LockComponent] Private key regeneration took ${new Date().getTime() - startRegeneration}ms`,
);
if (this.clientType === "browser") {
const previousUrl = this.lockComponentService.getPreviousUrl();
/**
* In a passkey flow, the `previousUrl` will still be `/fido2?<queryParams>` at this point
* because the `/lock` route doesn't save the URL in the `BrowserRouterService`. This is
* handled by the `doNotSaveUrl` property on the `/lock` route in `app-routing.module.ts`.
*/
if (previousUrl) {
await this.router.navigateByUrl(previousUrl);
return;
}
}
// determine success route based on client type
// The disable-redirect parameter allows callers to prevent automatic navigation after unlock,
// useful when the lock component is used in contexts where custom post-unlock behavior is needed
// such as passkey modals.
if (
this.clientType != null &&
this.activatedRoute.snapshot.paramMap.get("disable-redirect") === null
) {
const successRoute = clientTypeToSuccessRouteRecord[this.clientType];
await this.router.navigate([successRoute]);
}
if (
this.shouldClosePopout &&
this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension
) {
this.lockComponentService.closeBrowserExtensionPopout();
}
}
/**
* Checks if the master password meets the enforced policy requirements
* If not, returns false
*/
private requirePasswordChange(masterPassword: string): boolean {
if (
this.enforcedMasterPasswordOptions == undefined ||
!this.enforcedMasterPasswordOptions.enforceOnLogin ||
this.activeAccount == null
) {
return false;
}
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
this.activeAccount.email,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
masterPassword,
this.enforcedMasterPasswordOptions,
);
}
// -----------------------------------------------------------------------------------------------
// Desktop methods:
// -----------------------------------------------------------------------------------------------
async desktopOnInit() {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
// TODO: move this into a WindowService and subscribe to messages via MessageListener service.
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
}
private async desktopAutoPromptBiometrics() {
if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) {
return;
}
if (!(await this.biometricService.getShouldAutopromptNow())) {
return;
}
// prevent the biometric prompt from showing if the user has already cancelled it
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
return;
}
const windowVisible = await this.lockComponentService.isWindowVisible();
if (windowVisible) {
this.biometricAsked = true;
await this.unlockViaBiometrics();
}
}
onWindowHidden() {
this.showPassword = false;
}
private focusInput() {
if (this.unlockOptions) {
document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus();
}
}
// -----------------------------------------------------------------------------------------------
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
if (this.clientType === "desktop") {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
}
get biometricsAvailable(): boolean {
return this.unlockOptions?.biometrics.enabled ?? false;
}
get showBiometrics(): boolean {
if (this.unlockOptions == null) {
return false;
}
return (
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.PlatformUnsupported &&
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.NotEnabledLocally
);
}
get biometricUnavailabilityReason(): string {
switch (this.unlockOptions?.biometrics.biometricsStatus) {
case BiometricsStatus.Available:
return "";
case BiometricsStatus.UnlockNeeded:
return this.i18nService.t("biometricsStatusHelptextUnlockNeeded");
case BiometricsStatus.HardwareUnavailable:
return this.i18nService.t("biometricsStatusHelptextHardwareUnavailable");
case BiometricsStatus.AutoSetupNeeded:
return this.i18nService.t("biometricsStatusHelptextAutoSetupNeeded");
case BiometricsStatus.ManualSetupNeeded:
return this.i18nService.t("biometricsStatusHelptextManualSetupNeeded");
case BiometricsStatus.NotEnabledInConnectedDesktopApp:
return this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop",
this.activeAccount?.email,
);
case BiometricsStatus.NotEnabledLocally:
return this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop",
this.activeAccount?.email,
);
case BiometricsStatus.DesktopDisconnected:
return this.i18nService.t("biometricsStatusHelptextDesktopDisconnected");
default:
return (
this.i18nService.t("biometricsStatusHelptextUnavailableReasonUnknown") +
this.unlockOptions?.biometrics.biometricsStatus
);
}
}
}