diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 3a5583f5468..99ca31bafd5 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -5019,6 +5019,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/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html
index ebf79af644c..d835497d9be 100644
--- a/apps/browser/src/auth/popup/settings/account-security.component.html
+++ b/apps/browser/src/auth/popup/settings/account-security.component.html
@@ -5,6 +5,13 @@
+
diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts
index abe642970bb..56b18068778 100644
--- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts
+++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts
@@ -4,7 +4,10 @@ import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
+import { CollectionService } from "@bitwarden/admin-console/common";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
@@ -16,14 +19,18 @@ import {
VaultTimeoutStringType,
VaultTimeoutAction,
} from "@bitwarden/common/key-management/vault-timeout";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { MessageSender } from "@bitwarden/common/platform/messaging";
import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { StateProvider } from "@bitwarden/common/platform/state";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
@@ -71,6 +78,13 @@ describe("AccountSecurityComponent", () => {
{ provide: UserVerificationService, useValue: mock() },
{ provide: VaultTimeoutService, useValue: mock() },
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
+ { provide: StateProvider, useValue: mock() },
+ { provide: CipherService, useValue: mock() },
+ { provide: ApiService, useValue: mock() },
+ { provide: LogService, useValue: mock() },
+ { provide: OrganizationService, useValue: mock() },
+ { provide: CollectionService, useValue: mock() },
+ { provide: ConfigService, useValue: mock() },
],
})
.overrideComponent(AccountSecurityComponent, {
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..b2380a1a47e 100644
--- a/apps/browser/src/auth/popup/settings/account-security.component.ts
+++ b/apps/browser/src/auth/popup/settings/account-security.component.ts
@@ -22,6 +22,7 @@ import {
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -64,6 +65,7 @@ import {
BiometricStateService,
BiometricsStatus,
} from "@bitwarden/key-management";
+import { SpotlightComponent } from "@bitwarden/vault";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
@@ -96,6 +98,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
SectionComponent,
SectionHeaderComponent,
SelectModule,
+ SpotlightComponent,
TypographyModule,
VaultTimeoutInputComponent,
],
@@ -120,6 +123,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
enableAutoBiometricsPrompt: true,
});
+ protected showAccountSecurityNudge$: Observable =
+ this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) =>
+ this.vaultNudgesService.showNudgeSpotlight$(NudgeType.AccountSecurity, userId),
+ ),
+ );
+
private refreshTimeoutSettings$ = new BehaviorSubject(undefined);
private destroy$ = new Subject();
@@ -142,6 +153,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
private biometricStateService: BiometricStateService,
private toastService: ToastService,
private biometricsService: BiometricsService,
+ private vaultNudgesService: NudgesService,
) {}
async ngOnInit() {
@@ -402,6 +414,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
}
}
+ protected async dismissAccountSecurityNudge() {
+ const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
+ if (!activeAccount) {
+ return;
+ }
+ await this.vaultNudgesService.dismissNudge(NudgeType.AccountSecurity, activeAccount.id);
+ }
+
async saveVaultTimeoutAction(value: VaultTimeoutAction) {
if (value === VaultTimeoutAction.LogOut) {
const confirmed = await this.dialogService.openSimpleDialog({
@@ -453,6 +473,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(NudgeType.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 dc53f95a7cf..0b2e84712a4 100644
--- a/apps/browser/src/tools/popup/settings/settings-v2.component.html
+++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html
@@ -82,7 +82,7 @@
{{ "downloadBitwardenOnAllDevices" | i18n }}
= this.authenticatedAccount$.pipe(
+ showDownloadBitwardenNudge$: Observable = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
),
diff --git a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts
new file mode 100644
index 00000000000..862beb333d4
--- /dev/null
+++ b/libs/angular/src/vault/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, NudgeType } from "../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: NudgeType, 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/angular/src/vault/services/custom-nudges-services/index.ts b/libs/angular/src/vault/services/custom-nudges-services/index.ts
index 2e9ade985cc..e94b8bf71e5 100644
--- a/libs/angular/src/vault/services/custom-nudges-services/index.ts
+++ b/libs/angular/src/vault/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/angular/src/vault/services/nudges.service.spec.ts b/libs/angular/src/vault/services/nudges.service.spec.ts
index 4eee349cf10..4edd57f5428 100644
--- a/libs/angular/src/vault/services/nudges.service.spec.ts
+++ b/libs/angular/src/vault/services/nudges.service.spec.ts
@@ -2,8 +2,10 @@ import { TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
+import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateProvider } from "@bitwarden/common/platform/state";
@@ -74,6 +76,14 @@ describe("Vault Nudges Service", () => {
provide: LogService,
useValue: mock(),
},
+ {
+ provide: PinServiceAbstraction,
+ useValue: mock(),
+ },
+ {
+ provide: VaultTimeoutSettingsService,
+ useValue: mock(),
+ },
],
});
});
diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts
index 6784e9c7b4e..55e6009a8e0 100644
--- a/libs/angular/src/vault/services/nudges.service.ts
+++ b/libs/angular/src/vault/services/nudges.service.ts
@@ -12,6 +12,7 @@ import {
AutofillNudgeService,
DownloadBitwardenNudgeService,
NewItemNudgeService,
+ AccountSecurityNudgeService,
} from "./custom-nudges-services";
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
@@ -32,6 +33,7 @@ export enum NudgeType {
EmptyVaultNudge = "empty-vault-nudge",
HasVaultItems = "has-vault-items",
AutofillNudge = "autofill-nudge",
+ AccountSecurity = "account-security",
DownloadBitwarden = "download-bitwarden",
NewLoginItemStatus = "new-login-item-status",
NewCardItemStatus = "new-card-item-status",
@@ -61,6 +63,7 @@ export class NudgesService {
private customNudgeServices: Partial> = {
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
+ [NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
[NudgeType.AutofillNudge]: inject(AutofillNudgeService),
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
[NudgeType.NewLoginItemStatus]: this.newItemNudgeService,