mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
[PM-10741] Refactor biometrics interface & add dynamic status (#10973)
This commit is contained in:
@@ -3,8 +3,4 @@
|
||||
*/
|
||||
|
||||
export { LockComponent } from "./lock/components/lock.component";
|
||||
export {
|
||||
LockComponentService,
|
||||
BiometricsDisableReason,
|
||||
UnlockOptions,
|
||||
} from "./lock/services/lock-component.service";
|
||||
export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service";
|
||||
|
||||
@@ -86,12 +86,13 @@
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
@@ -156,12 +157,13 @@
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,16 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
firstValueFrom,
|
||||
interval,
|
||||
mergeMap,
|
||||
Subject,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
@@ -27,7 +36,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -42,6 +50,8 @@ import {
|
||||
import {
|
||||
KeyService,
|
||||
BiometricStateService,
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
@@ -115,9 +125,6 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
private deferFocus: boolean = null;
|
||||
private biometricAsked = false;
|
||||
|
||||
// Browser extension properties:
|
||||
private isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
||||
defaultUnlockOptionSetForUser = false;
|
||||
|
||||
unlockingViaBiometrics = false;
|
||||
@@ -144,6 +151,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||
|
||||
private biometricService: BiometricsService,
|
||||
|
||||
private lockComponentService: LockComponentService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
|
||||
@@ -157,14 +166,31 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
// Listen for active account changes
|
||||
this.listenForActiveAccountChanges();
|
||||
|
||||
this.listenForUnlockOptionsChanges();
|
||||
|
||||
// Identify client
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
|
||||
if (this.clientType === "desktop") {
|
||||
await this.desktopOnInit();
|
||||
} else if (this.clientType === ClientType.Browser) {
|
||||
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
|
||||
}
|
||||
}
|
||||
|
||||
private listenForUnlockOptionsChanges() {
|
||||
interval(1000)
|
||||
.pipe(
|
||||
mergeMap(async () => {
|
||||
this.unlockOptions = await firstValueFrom(
|
||||
this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id),
|
||||
);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
// Base component methods
|
||||
private listenForActiveUnlockOptionChanges() {
|
||||
this.activeUnlockOption$
|
||||
@@ -234,7 +260,6 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.unlockOptions = null;
|
||||
this.activeUnlockOption = null;
|
||||
this.formGroup = null; // new form group will be created based on new active unlock option
|
||||
this.isInitialLockScreen = true;
|
||||
|
||||
// Desktop properties:
|
||||
this.biometricAsked = false;
|
||||
@@ -276,8 +301,9 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
if (
|
||||
this.unlockOptions.biometrics.enabled &&
|
||||
autoPromptBiometrics &&
|
||||
this.isInitialLockScreen // only autoprompt biometrics on initial lock screen
|
||||
(await this.biometricService.getShouldAutopromptNow())
|
||||
) {
|
||||
await this.biometricService.setShouldAutopromptNow(false);
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
@@ -316,8 +342,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
|
||||
try {
|
||||
await this.biometricStateService.setUserPromptCancelled();
|
||||
const userKey = await this.keyService.getUserKeyFromStorage(
|
||||
KeySuffixOptions.Biometric,
|
||||
const userKey = await this.biometricService.unlockWithBiometricsForUser(
|
||||
this.activeAccount.id,
|
||||
);
|
||||
|
||||
@@ -587,6 +612,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
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(() => {
|
||||
@@ -617,6 +644,10 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
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;
|
||||
@@ -650,4 +681,47 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
get biometricsAvailable(): boolean {
|
||||
return this.unlockOptions.biometrics.enabled;
|
||||
}
|
||||
|
||||
get showBiometrics(): boolean {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export enum BiometricsDisableReason {
|
||||
NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem",
|
||||
EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable",
|
||||
SystemBiometricsUnavailable = "SystemBiometricsUnavailable",
|
||||
}
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics"
|
||||
export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption];
|
||||
@@ -26,7 +21,7 @@ export type UnlockOptions = {
|
||||
};
|
||||
biometrics: {
|
||||
enabled: boolean;
|
||||
disableReason: BiometricsDisableReason | null;
|
||||
biometricsStatus: BiometricsStatus;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user