diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index dcdfe7df4d6..56bc7e38cfb 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -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"
},
diff --git a/apps/browser/src/auth/popup/components/set-pin.component.html b/apps/browser/src/auth/popup/components/set-pin.component.html
index 58cb42456ee..dd335c58dca 100644
--- a/apps/browser/src/auth/popup/components/set-pin.component.html
+++ b/apps/browser/src/auth/popup/components/set-pin.component.html
@@ -26,7 +26,7 @@
-
+
diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts
index ede044b21de..9935804e7d4 100644
--- a/apps/browser/src/auth/popup/settings/account-security.component.ts
+++ b/apps/browser/src/auth/popup/settings/account-security.component.ts
@@ -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 =
+ this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) =>
+ this.vaultNudgesService.showNudge$(VaultNudgeType.AccountSecurity, userId),
+ ),
+ );
+
private refreshTimeoutSettings$ = new BehaviorSubject(undefined);
private destroy$ = new Subject();
@@ -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();
}
diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html
index 8d31ccf8371..1ed51d8a7c9 100644
--- a/apps/browser/src/tools/popup/settings/settings-v2.component.html
+++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html
@@ -7,10 +7,19 @@
-
+
- {{ "accountSecurity" | i18n }}
+
+
{{ "accountSecurity" | i18n }}
+
1
+
diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts
index be05452529a..7ab0b6aceea 100644
--- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts
+++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts
@@ -51,6 +51,12 @@ export class SettingsV2Component implements OnInit {
shareReplay({ bufferSize: 1, refCount: true }),
);
+ accountSecurityNudgeStatus$: Observable = this.authenticatedAccount$.pipe(
+ switchMap((account) =>
+ this.vaultNudgesService.showNudge$(VaultNudgeType.AccountSecurity, account.id),
+ ),
+ );
+
downloadBitwardenNudgeStatus$: Observable = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id),
diff --git a/libs/vault/src/services/custom-nudges-services/account-security-nudge.service.ts b/libs/vault/src/services/custom-nudges-services/account-security-nudge.service.ts
new file mode 100644
index 00000000000..bcdd644553b
--- /dev/null
+++ b/libs/vault/src/services/custom-nudges-services/account-security-nudge.service.ts
@@ -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 {
+ 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,
+ };
+ }),
+ );
+ }
+}
diff --git a/libs/vault/src/services/custom-nudges-services/index.ts b/libs/vault/src/services/custom-nudges-services/index.ts
index 2e9ade985cc..e94b8bf71e5 100644
--- a/libs/vault/src/services/custom-nudges-services/index.ts
+++ b/libs/vault/src/services/custom-nudges-services/index.ts
@@ -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";
diff --git a/libs/vault/src/services/vault-nudges.service.ts b/libs/vault/src/services/vault-nudges.service.ts
index d27cd09e954..d445ec7a17c 100644
--- a/libs/vault/src/services/vault-nudges.service.ts
+++ b/libs/vault/src/services/vault-nudges.service.ts
@@ -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),
};
/**