mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-18801] - account security nudge (#14771)
* account security nudge
* fix messages.json
* fix tests
* fix logic for account security item
* fix tests
* adjust account security nudge work to updated nudge service
* fix account security nudge
* remove unused code. do not show account security badge
* include ff and safari in link html
* fix import
* Revert "include ff and safari in link html"
This reverts commit cd12a36274.
This commit is contained in:
@@ -5019,6 +5019,15 @@
|
|||||||
"biometricsStatusHelptextUnavailableReasonUnknown": {
|
"biometricsStatusHelptextUnavailableReasonUnknown": {
|
||||||
"message": "Biometric unlock is currently unavailable for an unknown reason."
|
"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": {
|
"authenticating": {
|
||||||
"message": "Authenticating"
|
"message": "Authenticating"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</popup-header>
|
</popup-header>
|
||||||
|
|
||||||
|
<bit-spotlight
|
||||||
|
*ngIf="showAccountSecurityNudge$ | async"
|
||||||
|
[title]="'unlockVault' | i18n"
|
||||||
|
[subtitle]="'unlockVaultDesc' | i18n"
|
||||||
|
(onDismiss)="dismissAccountSecurityNudge()"
|
||||||
|
class="tw-mb-6"
|
||||||
|
></bit-spotlight>
|
||||||
<div [formGroup]="form">
|
<div [formGroup]="form">
|
||||||
<bit-section>
|
<bit-section>
|
||||||
<bit-section-header>
|
<bit-section-header>
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { By } from "@angular/platform-browser";
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, of } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/auth/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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
@@ -16,14 +19,18 @@ import {
|
|||||||
VaultTimeoutStringType,
|
VaultTimeoutStringType,
|
||||||
VaultTimeoutAction,
|
VaultTimeoutAction,
|
||||||
} from "@bitwarden/common/key-management/vault-timeout";
|
} 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
@@ -71,6 +78,13 @@ describe("AccountSecurityComponent", () => {
|
|||||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||||
{ provide: VaultTimeoutService, useValue: mock<VaultTimeoutService>() },
|
{ provide: VaultTimeoutService, useValue: mock<VaultTimeoutService>() },
|
||||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||||
|
{ provide: StateProvider, useValue: mock<StateProvider>() },
|
||||||
|
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||||
|
{ provide: ApiService, useValue: mock<ApiService>() },
|
||||||
|
{ provide: LogService, useValue: mock<LogService>() },
|
||||||
|
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||||
|
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||||
|
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(AccountSecurityComponent, {
|
.overrideComponent(AccountSecurityComponent, {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||||
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@@ -64,6 +65,7 @@ import {
|
|||||||
BiometricStateService,
|
BiometricStateService,
|
||||||
BiometricsStatus,
|
BiometricsStatus,
|
||||||
} from "@bitwarden/key-management";
|
} from "@bitwarden/key-management";
|
||||||
|
import { SpotlightComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
@@ -96,6 +98,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
|||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
SelectModule,
|
SelectModule,
|
||||||
|
SpotlightComponent,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
],
|
],
|
||||||
@@ -120,6 +123,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
enableAutoBiometricsPrompt: true,
|
enableAutoBiometricsPrompt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected showAccountSecurityNudge$: Observable<boolean> =
|
||||||
|
this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) =>
|
||||||
|
this.vaultNudgesService.showNudgeSpotlight$(NudgeType.AccountSecurity, userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -142,6 +153,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private biometricsService: BiometricsService,
|
private biometricsService: BiometricsService,
|
||||||
|
private vaultNudgesService: NudgesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
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) {
|
async saveVaultTimeoutAction(value: VaultTimeoutAction) {
|
||||||
if (value === VaultTimeoutAction.LogOut) {
|
if (value === VaultTimeoutAction.LogOut) {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
@@ -453,6 +473,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
||||||
const requireReprompt = (await this.pinService.getPinLockType(userId)) == "EPHEMERAL";
|
const requireReprompt = (await this.pinService.getPinLockType(userId)) == "EPHEMERAL";
|
||||||
this.form.controls.pinLockWithMasterPassword.setValue(requireReprompt, { emitEvent: false });
|
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 {
|
} else {
|
||||||
await this.vaultTimeoutSettingsService.clear();
|
await this.vaultTimeoutSettingsService.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<div class="tw-flex tw-items-center tw-justify-center">
|
<div class="tw-flex tw-items-center tw-justify-center">
|
||||||
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
|
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
|
||||||
<span
|
<span
|
||||||
*ngIf="downloadBitwardenNudgeStatus$ | async"
|
*ngIf="showDownloadBitwardenNudge$ | async"
|
||||||
bitBadge
|
bitBadge
|
||||||
variant="notification"
|
variant="notification"
|
||||||
[attr.aria-label]="'nudgeBadgeAria' | i18n"
|
[attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class SettingsV2Component implements OnInit {
|
|||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
downloadBitwardenNudgeStatus$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
showDownloadBitwardenNudge$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((account) =>
|
||||||
this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
|
this.nudgesService.showNudgeBadge$(NudgeType.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, 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<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 "./autofill-nudge.service";
|
||||||
|
export * from "./account-security-nudge.service";
|
||||||
export * from "./has-items-nudge.service";
|
export * from "./has-items-nudge.service";
|
||||||
export * from "./download-bitwarden-nudge.service";
|
export * from "./download-bitwarden-nudge.service";
|
||||||
export * from "./empty-vault-nudge.service";
|
export * from "./empty-vault-nudge.service";
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { TestBed } from "@angular/core/testing";
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, of } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
@@ -74,6 +76,14 @@ describe("Vault Nudges Service", () => {
|
|||||||
provide: LogService,
|
provide: LogService,
|
||||||
useValue: mock<LogService>(),
|
useValue: mock<LogService>(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PinServiceAbstraction,
|
||||||
|
useValue: mock<PinServiceAbstraction>(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: VaultTimeoutSettingsService,
|
||||||
|
useValue: mock<VaultTimeoutSettingsService>(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
AutofillNudgeService,
|
AutofillNudgeService,
|
||||||
DownloadBitwardenNudgeService,
|
DownloadBitwardenNudgeService,
|
||||||
NewItemNudgeService,
|
NewItemNudgeService,
|
||||||
|
AccountSecurityNudgeService,
|
||||||
} from "./custom-nudges-services";
|
} from "./custom-nudges-services";
|
||||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export enum NudgeType {
|
|||||||
EmptyVaultNudge = "empty-vault-nudge",
|
EmptyVaultNudge = "empty-vault-nudge",
|
||||||
HasVaultItems = "has-vault-items",
|
HasVaultItems = "has-vault-items",
|
||||||
AutofillNudge = "autofill-nudge",
|
AutofillNudge = "autofill-nudge",
|
||||||
|
AccountSecurity = "account-security",
|
||||||
DownloadBitwarden = "download-bitwarden",
|
DownloadBitwarden = "download-bitwarden",
|
||||||
NewLoginItemStatus = "new-login-item-status",
|
NewLoginItemStatus = "new-login-item-status",
|
||||||
NewCardItemStatus = "new-card-item-status",
|
NewCardItemStatus = "new-card-item-status",
|
||||||
@@ -61,6 +63,7 @@ export class NudgesService {
|
|||||||
private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
|
private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
|
||||||
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||||
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||||
|
[NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
|
||||||
[NudgeType.AutofillNudge]: inject(AutofillNudgeService),
|
[NudgeType.AutofillNudge]: inject(AutofillNudgeService),
|
||||||
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
||||||
[NudgeType.NewLoginItemStatus]: this.newItemNudgeService,
|
[NudgeType.NewLoginItemStatus]: this.newItemNudgeService,
|
||||||
|
|||||||
Reference in New Issue
Block a user