1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 19:34:03 +00:00

account security nudge

This commit is contained in:
jaasen-livefront
2025-05-13 18:47:20 -07:00
parent bacd1fb999
commit 567e4a1efb
9 changed files with 118 additions and 4 deletions

View File

@@ -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"
},

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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),

View File

@@ -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,
};
}),
);
}
}

View File

@@ -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";

View File

@@ -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),
};
/**