mirror of
https://github.com/bitwarden/browser
synced 2026-02-20 19:34:03 +00:00
account security nudge
This commit is contained in:
@@ -1090,7 +1090,7 @@
|
||||
},
|
||||
"notificationLoginSaveConfirmation": {
|
||||
"message": "saved to Bitwarden.",
|
||||
|
||||
|
||||
"description": "Shown to user after item is saved."
|
||||
},
|
||||
"notificationLoginUpdatedConfirmation": {
|
||||
@@ -5009,6 +5009,15 @@
|
||||
"biometricsStatusHelptextUnavailableReasonUnknown": {
|
||||
"message": "Biometric unlock is currently unavailable for an unknown reason."
|
||||
},
|
||||
"unlockVault": {
|
||||
"message": "Unlock your vault in seconds"
|
||||
},
|
||||
"unlockVaultDesc": {
|
||||
"message": "You can customize your unlock and timeout settings to more quickly access your vault."
|
||||
},
|
||||
"unlockPinSet": {
|
||||
"message": "Unlock PIN set"
|
||||
},
|
||||
"authenticating": {
|
||||
"message": "Authenticating"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" onClick="show">
|
||||
<span>{{ "setYourPinButton" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<bit-spotlight
|
||||
*ngIf="(accountSecurityNudgeStatus$ | async)?.hasSpotlightDismissed === false"
|
||||
[title]="'unlockVault' | i18n"
|
||||
[subtitle]="'unlockVaultDesc' | i18n"
|
||||
(onDismiss)="dismissAccountSecurityNudge()"
|
||||
class="tw-mb-6"
|
||||
></bit-spotlight>
|
||||
<div [formGroup]="form">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
|
||||
@@ -64,6 +64,12 @@ import {
|
||||
BiometricStateService,
|
||||
BiometricsStatus,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
NudgeStatus,
|
||||
SpotlightComponent,
|
||||
VaultNudgesService,
|
||||
VaultNudgeType,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
@@ -96,6 +102,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
SelectModule,
|
||||
SpotlightComponent,
|
||||
TypographyModule,
|
||||
VaultTimeoutInputComponent,
|
||||
],
|
||||
@@ -120,6 +127,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
enableAutoBiometricsPrompt: true,
|
||||
});
|
||||
|
||||
protected accountSecurityNudgeStatus$: Observable<NudgeStatus> =
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.vaultNudgesService.showNudge$(VaultNudgeType.AccountSecurity, userId),
|
||||
),
|
||||
);
|
||||
|
||||
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -142,6 +157,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
private biometricStateService: BiometricStateService,
|
||||
private toastService: ToastService,
|
||||
private biometricsService: BiometricsService,
|
||||
private vaultNudgesService: VaultNudgesService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -402,6 +418,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
protected async dismissAccountSecurityNudge() {
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!activeAccount) {
|
||||
return;
|
||||
}
|
||||
await this.vaultNudgesService.dismissNudge(VaultNudgeType.AccountSecurity, activeAccount.id);
|
||||
}
|
||||
|
||||
async saveVaultTimeoutAction(value: VaultTimeoutAction) {
|
||||
if (value === VaultTimeoutAction.LogOut) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
@@ -453,6 +477,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
||||
const requireReprompt = (await this.pinService.getPinLockType(userId)) == "EPHEMERAL";
|
||||
this.form.controls.pinLockWithMasterPassword.setValue(requireReprompt, { emitEvent: false });
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("unlockPinSet"),
|
||||
});
|
||||
await this.vaultNudgesService.dismissNudge(VaultNudgeType.AccountSecurity, userId);
|
||||
} else {
|
||||
await this.vaultTimeoutSettingsService.clear();
|
||||
}
|
||||
|
||||
@@ -7,10 +7,19 @@
|
||||
</popup-header>
|
||||
|
||||
<bit-item-group>
|
||||
<bit-item>
|
||||
<bit-item *ngIf="isNudgeFeatureEnabled$ | async">
|
||||
<a bit-item-content routerLink="/account-security">
|
||||
<i slot="start" class="bwi bwi-lock" aria-hidden="true"></i>
|
||||
{{ "accountSecurity" | i18n }}
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<p class="tw-pr-2">{{ "accountSecurity" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="(accountSecurityNudgeStatus$ | async)?.hasBadgeDismissed === false"
|
||||
bitBadge
|
||||
variant="notification"
|
||||
[attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||
>1</span
|
||||
>
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
|
||||
@@ -51,6 +51,12 @@ export class SettingsV2Component implements OnInit {
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
accountSecurityNudgeStatus$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.vaultNudgesService.showNudge$(VaultNudgeType.AccountSecurity, account.id),
|
||||
),
|
||||
);
|
||||
|
||||
downloadBitwardenNudgeStatus$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, map } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
private vaultProfileService = inject(VaultProfileService);
|
||||
private logService = inject(LogService);
|
||||
private pinService = inject(PinServiceAbstraction);
|
||||
private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
|
||||
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
catchError(() => {
|
||||
this.logService.error("Failed to load profile date:");
|
||||
// Default to today to ensure the nudge is shown in case of an error
|
||||
return of(new Date());
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
profileDate$,
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
from(this.pinService.isPinSet(userId)),
|
||||
from(this.vaultTimeoutSettingsService.isBiometricLockSet(userId)),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff, isPinSet, isBiometricLockSet]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
const hideNudge = profileOlderThanCutoff || isPinSet || isBiometricLockSet;
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || hideNudge,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./autofill-nudge.service";
|
||||
export * from "./account-security-nudge.service";
|
||||
export * from "./has-items-nudge.service";
|
||||
export * from "./download-bitwarden-nudge.service";
|
||||
export * from "./empty-vault-nudge.service";
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AutofillNudgeService,
|
||||
DownloadBitwardenNudgeService,
|
||||
NewItemNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
|
||||
@@ -38,6 +39,7 @@ export enum VaultNudgeType {
|
||||
newIdentityItemStatus = "new-identity-item-status",
|
||||
newNoteItemStatus = "new-note-item-status",
|
||||
newSshItemStatus = "new-ssh-item-status",
|
||||
AccountSecurity = "account-security",
|
||||
}
|
||||
|
||||
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||
@@ -68,6 +70,7 @@ export class VaultNudgesService {
|
||||
[VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newNoteItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newSshItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user