1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-21663] nudge service name refactor (#14789)

* update names of vault nudge service and their corresponding files, convert components using showNudge$ to instead target spotlight and badges directly with new observables. Core logic for dismiss remains the same
This commit is contained in:
Jason Ng
2025-05-15 15:10:38 -04:00
committed by GitHub
parent 82d0925f4e
commit ee4c3cfd94
20 changed files with 199 additions and 152 deletions

View File

@@ -4,14 +4,14 @@ import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit } from "@angular/core"; import { Component, DestroyRef, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { import {
FormBuilder,
FormControl,
FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
FormBuilder,
FormGroup,
FormControl,
} from "@angular/forms"; } from "@angular/forms";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { Observable, filter, firstValueFrom, map, switchMap } from "rxjs"; import { filter, firstValueFrom, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -55,7 +55,7 @@ import {
SelectModule, SelectModule,
TypographyModule, TypographyModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { SpotlightComponent, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault"; import { NudgesService, NudgeType, SpotlightComponent } from "@bitwarden/vault";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service"; import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
@@ -108,9 +108,7 @@ export class AutofillComponent implements OnInit {
protected showSpotlightNudge$: Observable<boolean> = this.accountService.activeAccount$.pipe( protected showSpotlightNudge$: Observable<boolean> = this.accountService.activeAccount$.pipe(
filter((account): account is Account => account !== null), filter((account): account is Account => account !== null),
switchMap((account) => switchMap((account) =>
this.vaultNudgesService this.nudgesService.showNudgeSpotlight$(NudgeType.AutofillNudge, account.id),
.showNudge$(VaultNudgeType.AutofillNudge, account.id)
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed)),
), ),
); );
@@ -155,7 +153,7 @@ export class AutofillComponent implements OnInit {
private configService: ConfigService, private configService: ConfigService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private vaultNudgesService: VaultNudgesService, private nudgesService: NudgesService,
private accountService: AccountService, private accountService: AccountService,
private autofillBrowserSettingsService: AutofillBrowserSettingsService, private autofillBrowserSettingsService: AutofillBrowserSettingsService,
) { ) {
@@ -343,8 +341,8 @@ export class AutofillComponent implements OnInit {
} }
async dismissSpotlight() { async dismissSpotlight() {
await this.vaultNudgesService.dismissNudge( await this.nudgesService.dismissNudge(
VaultNudgeType.AutofillNudge, NudgeType.AutofillNudge,
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
); );
} }

View File

@@ -6,7 +6,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Icons } from "@bitwarden/components"; import { Icons } from "@bitwarden/components";
import { VaultNudgesService } from "@bitwarden/vault"; import { NudgesService } from "@bitwarden/vault";
import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component"; import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component";
@@ -18,7 +18,7 @@ import { NavButton } from "../platform/popup/layout/popup-tab-navigation.compone
export class TabsV2Component { export class TabsV2Component {
private hasActiveBadges$ = this.accountService.activeAccount$ private hasActiveBadges$ = this.accountService.activeAccount$
.pipe(getUserId) .pipe(getUserId)
.pipe(switchMap((userId) => this.vaultNudgesService.hasActiveBadges$(userId))); .pipe(switchMap((userId) => this.nudgesService.hasActiveBadges$(userId)));
protected navButtons$: Observable<NavButton[]> = combineLatest([ protected navButtons$: Observable<NavButton[]> = combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
this.hasActiveBadges$, this.hasActiveBadges$,
@@ -54,7 +54,7 @@ export class TabsV2Component {
}), }),
); );
constructor( constructor(
private vaultNudgesService: VaultNudgesService, private nudgesService: NudgesService,
private accountService: AccountService, private accountService: AccountService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) {} ) {}

View File

@@ -41,7 +41,7 @@
<a <a
bit-item-content bit-item-content
routerLink="/vault-settings" routerLink="/vault-settings"
(click)="dismissBadge(VaultNudgeType.EmptyVaultNudge)" (click)="dismissBadge(NudgeType.EmptyVaultNudge)"
> >
<i slot="start" class="bwi bwi-vault" aria-hidden="true"></i> <i slot="start" class="bwi bwi-vault" aria-hidden="true"></i>
<div class="tw-flex tw-items-center tw-justify-center"> <div class="tw-flex tw-items-center tw-justify-center">
@@ -51,7 +51,7 @@
Will make this dynamic when more nudges are added Will make this dynamic when more nudges are added
--> -->
<span <span
*ngIf="!(showVaultBadge$ | async)?.hasBadgeDismissed" *ngIf="showVaultBadge$ | async"
bitBadge bitBadge
variant="notification" variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n" [attr.aria-label]="'nudgeBadgeAria' | i18n"
@@ -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)?.hasBadgeDismissed === false" *ngIf="downloadBitwardenNudgeStatus$ | async"
bitBadge bitBadge
variant="notification" variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n" [attr.aria-label]="'nudgeBadgeAria' | i18n"

View File

@@ -17,7 +17,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { BadgeComponent, ItemModule } from "@bitwarden/components"; import { BadgeComponent, ItemModule } from "@bitwarden/components";
import { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault"; import { NudgesService, NudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service"; import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
@@ -42,7 +42,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
], ],
}) })
export class SettingsV2Component implements OnInit { export class SettingsV2Component implements OnInit {
VaultNudgeType = VaultNudgeType; NudgeType = NudgeType;
activeUserId: UserId | null = null; activeUserId: UserId | null = null;
protected isBrowserAutofillSettingOverridden = false; protected isBrowserAutofillSettingOverridden = false;
@@ -51,15 +51,15 @@ export class SettingsV2Component implements OnInit {
shareReplay({ bufferSize: 1, refCount: true }), shareReplay({ bufferSize: 1, refCount: true }),
); );
downloadBitwardenNudgeStatus$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe( downloadBitwardenNudgeStatus$: Observable<boolean> = this.authenticatedAccount$.pipe(
switchMap((account) => switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id), this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
), ),
); );
showVaultBadge$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe( showVaultBadge$: Observable<boolean> = this.authenticatedAccount$.pipe(
switchMap((account) => switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, account.id), this.nudgesService.showNudgeBadge$(NudgeType.EmptyVaultNudge, account.id),
), ),
); );
@@ -68,9 +68,9 @@ export class SettingsV2Component implements OnInit {
this.authenticatedAccount$, this.authenticatedAccount$,
]).pipe( ]).pipe(
switchMap(([defaultBrowserAutofillDisabled, account]) => switchMap(([defaultBrowserAutofillDisabled, account]) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.AutofillNudge, account.id).pipe( this.nudgesService.showNudgeBadge$(NudgeType.AutofillNudge, account.id).pipe(
map((nudgeStatus) => { map((badgeStatus) => {
return !defaultBrowserAutofillDisabled && nudgeStatus.hasBadgeDismissed === false; return !defaultBrowserAutofillDisabled && badgeStatus;
}), }),
), ),
), ),
@@ -81,7 +81,7 @@ export class SettingsV2Component implements OnInit {
); );
constructor( constructor(
private readonly vaultNudgesService: VaultNudgesService, private readonly nudgesService: NudgesService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService, private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
@@ -94,10 +94,10 @@ export class SettingsV2Component implements OnInit {
); );
} }
async dismissBadge(type: VaultNudgeType) { async dismissBadge(type: NudgeType) {
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) { if (await firstValueFrom(this.showVaultBadge$)) {
const account = await firstValueFrom(this.authenticatedAccount$); const account = await firstValueFrom(this.authenticatedAccount$);
await this.vaultNudgesService.dismissNudge(type, account.id as UserId, true); await this.nudgesService.dismissNudge(type, account.id as UserId, true);
} }
} }
} }

View File

@@ -36,7 +36,7 @@
[subtitle]="'emptyVaultNudgeBody' | i18n" [subtitle]="'emptyVaultNudgeBody' | i18n"
[buttonText]="'emptyVaultNudgeButton' | i18n" [buttonText]="'emptyVaultNudgeButton' | i18n"
(onButtonClick)="navigateToImport()" (onButtonClick)="navigateToImport()"
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.EmptyVaultNudge)" (onDismiss)="dismissVaultNudgeSpotlight(NudgeType.EmptyVaultNudge)"
> >
</bit-spotlight> </bit-spotlight>
</ng-container> </ng-container>
@@ -44,7 +44,7 @@
<div class="tw-mb-4" *ngIf="showHasItemsVaultSpotlight$ | async"> <div class="tw-mb-4" *ngIf="showHasItemsVaultSpotlight$ | async">
<bit-spotlight <bit-spotlight
[title]="'hasItemsVaultNudgeTitle' | i18n" [title]="'hasItemsVaultNudgeTitle' | i18n"
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.HasVaultItems)" (onDismiss)="dismissVaultNudgeSpotlight(NudgeType.HasVaultItems)"
> >
<ul class="tw-pl-4 tw-text-main" bitTypography="body2"> <ul class="tw-pl-4 tw-text-main" bitTypography="body2">
<li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li> <li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li>

View File

@@ -32,10 +32,10 @@ import {
} from "@bitwarden/components"; } from "@bitwarden/components";
import { import {
DecryptionFailureDialogComponent, DecryptionFailureDialogComponent,
NudgesService,
NudgeType,
SpotlightComponent, SpotlightComponent,
VaultIcons, VaultIcons,
VaultNudgesService,
VaultNudgeType,
} from "@bitwarden/vault"; } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
@@ -96,18 +96,16 @@ enum VaultState {
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
VaultNudgeType = VaultNudgeType; NudgeType = NudgeType;
cipherType = CipherType; cipherType = CipherType;
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
showEmptyVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe( showEmptyVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
switchMap((userId) => switchMap((userId) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, userId), this.nudgesService.showNudgeSpotlight$(NudgeType.EmptyVaultNudge, userId),
), ),
map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed),
); );
showHasItemsVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe( showHasItemsVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
switchMap((userId) => this.vaultNudgesService.showNudge$(VaultNudgeType.HasVaultItems, userId)), switchMap((userId) => this.nudgesService.showNudgeSpotlight$(NudgeType.HasVaultItems, userId)),
map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed),
); );
activeUserId: UserId | null = null; activeUserId: UserId | null = null;
@@ -159,7 +157,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private dialogService: DialogService, private dialogService: DialogService,
private vaultCopyButtonsService: VaultPopupCopyButtonsService, private vaultCopyButtonsService: VaultPopupCopyButtonsService,
private introCarouselService: IntroCarouselService, private introCarouselService: IntroCarouselService,
private vaultNudgesService: VaultNudgesService, private nudgesService: NudgesService,
private router: Router, private router: Router,
private i18nService: I18nService, private i18nService: I18nService,
) { ) {
@@ -229,8 +227,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
} }
} }
async dismissVaultNudgeSpotlight(type: VaultNudgeType) { async dismissVaultNudgeSpotlight(type: NudgeType) {
await this.vaultNudgesService.dismissNudge(type, this.activeUserId as UserId); await this.nudgesService.dismissNudge(type, this.activeUserId as UserId);
} }
protected readonly FeatureFlag = FeatureFlag; protected readonly FeatureFlag = FeatureFlag;

View File

@@ -7,7 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CardComponent, LinkModule, TypographyModule } from "@bitwarden/components"; import { CardComponent, LinkModule, TypographyModule } from "@bitwarden/components";
import { VaultNudgesService, VaultNudgeType } from "@bitwarden/vault"; import { NudgesService, NudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
@@ -32,12 +32,12 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
}) })
export class DownloadBitwardenComponent implements OnInit { export class DownloadBitwardenComponent implements OnInit {
constructor( constructor(
private vaultNudgeService: VaultNudgesService, private nudgesService: NudgesService,
private accountService: AccountService, private accountService: AccountService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.vaultNudgeService.dismissNudge(VaultNudgeType.DownloadBitwarden, userId); await this.nudgesService.dismissNudge(NudgeType.DownloadBitwarden, userId);
} }
} }

View File

@@ -199,7 +199,7 @@ export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk"); export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk");
export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk"); export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk");
export const VAULT_NUDGES_DISK = new StateDefinition("vaultNudges", "disk", { web: "disk-local" }); export const NUDGES_DISK = new StateDefinition("nudges", "disk", { web: "disk-local" });
export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
"vaultBrowserIntroCarousel", "vaultBrowserIntroCarousel",
"disk", "disk",

View File

@@ -36,7 +36,7 @@ import {
CipherFormGenerationService, CipherFormGenerationService,
NudgeStatus, NudgeStatus,
PasswordRepromptService, PasswordRepromptService,
VaultNudgesService, NudgesService,
} from "@bitwarden/vault"; } from "@bitwarden/vault";
// FIXME: remove `/apps` import from `/libs` // FIXME: remove `/apps` import from `/libs`
// FIXME: remove `src` and fix import // FIXME: remove `src` and fix import
@@ -144,7 +144,7 @@ export default {
], ],
providers: [ providers: [
{ {
provide: VaultNudgesService, provide: NudgesService,
useValue: { useValue: {
showNudge$: new BehaviorSubject({ showNudge$: new BehaviorSubject({
hasBadgeDismissed: true, hasBadgeDismissed: true,

View File

@@ -8,7 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/sdk-internal"; import { CipherType } from "@bitwarden/sdk-internal";
import { VaultNudgesService, VaultNudgeType } from "../../../services/vault-nudges.service"; import { NudgesService, NudgeType } from "../../../services/nudges.service";
import { NewItemNudgeComponent } from "./new-item-nudge.component"; import { NewItemNudgeComponent } from "./new-item-nudge.component";
@@ -18,19 +18,19 @@ describe("NewItemNudgeComponent", () => {
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let accountService: MockProxy<AccountService>; let accountService: MockProxy<AccountService>;
let vaultNudgesService: MockProxy<VaultNudgesService>; let nudgesService: MockProxy<NudgesService>;
beforeEach(async () => { beforeEach(async () => {
i18nService = mock<I18nService>({ t: (key: string) => key }); i18nService = mock<I18nService>({ t: (key: string) => key });
accountService = mock<AccountService>(); accountService = mock<AccountService>();
vaultNudgesService = mock<VaultNudgesService>(); nudgesService = mock<NudgesService>();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NewItemNudgeComponent, CommonModule], imports: [NewItemNudgeComponent, CommonModule],
providers: [ providers: [
{ provide: I18nService, useValue: i18nService }, { provide: I18nService, useValue: i18nService },
{ provide: AccountService, useValue: accountService }, { provide: AccountService, useValue: accountService },
{ provide: VaultNudgesService, useValue: vaultNudgesService }, { provide: NudgesService, useValue: nudgesService },
], ],
}).compileComponents(); }).compileComponents();
}); });
@@ -58,7 +58,7 @@ describe("NewItemNudgeComponent", () => {
expect(component.nudgeBody).toBe( expect(component.nudgeBody).toBe(
"newLoginNudgeBodyOne <strong>newLoginNudgeBodyBold</strong> newLoginNudgeBodyTwo", "newLoginNudgeBodyOne <strong>newLoginNudgeBodyBold</strong> newLoginNudgeBodyTwo",
); );
expect(component.dismissalNudgeType).toBe(VaultNudgeType.newLoginItemStatus); expect(component.dismissalNudgeType).toBe(NudgeType.NewLoginItemStatus);
}); });
it("should set nudge title and body for CipherType.Card type", async () => { it("should set nudge title and body for CipherType.Card type", async () => {
@@ -71,7 +71,7 @@ describe("NewItemNudgeComponent", () => {
expect(component.showNewItemSpotlight).toBe(true); expect(component.showNewItemSpotlight).toBe(true);
expect(component.nudgeTitle).toBe("newCardNudgeTitle"); expect(component.nudgeTitle).toBe("newCardNudgeTitle");
expect(component.nudgeBody).toBe("newCardNudgeBody"); expect(component.nudgeBody).toBe("newCardNudgeBody");
expect(component.dismissalNudgeType).toBe(VaultNudgeType.newCardItemStatus); expect(component.dismissalNudgeType).toBe(NudgeType.NewCardItemStatus);
}); });
it("should not show anything if spotlight has been dismissed", async () => { it("should not show anything if spotlight has been dismissed", async () => {
@@ -82,22 +82,19 @@ describe("NewItemNudgeComponent", () => {
await component.ngOnInit(); await component.ngOnInit();
expect(component.showNewItemSpotlight).toBe(false); expect(component.showNewItemSpotlight).toBe(false);
expect(component.dismissalNudgeType).toBe(VaultNudgeType.newIdentityItemStatus); expect(component.dismissalNudgeType).toBe(NudgeType.NewIdentityItemStatus);
}); });
it("should set showNewItemSpotlight to false when user dismisses spotlight", async () => { it("should set showNewItemSpotlight to false when user dismisses spotlight", async () => {
component.showNewItemSpotlight = true; component.showNewItemSpotlight = true;
component.dismissalNudgeType = VaultNudgeType.newLoginItemStatus; component.dismissalNudgeType = NudgeType.NewLoginItemStatus;
component.activeUserId = "test-user-id" as UserId; component.activeUserId = "test-user-id" as UserId;
const dismissSpy = jest.spyOn(vaultNudgesService, "dismissNudge").mockResolvedValue(); const dismissSpy = jest.spyOn(nudgesService, "dismissNudge").mockResolvedValue();
await component.dismissNewItemSpotlight(); await component.dismissNewItemSpotlight();
expect(component.showNewItemSpotlight).toBe(false); expect(component.showNewItemSpotlight).toBe(false);
expect(dismissSpy).toHaveBeenCalledWith( expect(dismissSpy).toHaveBeenCalledWith(NudgeType.NewLoginItemStatus, component.activeUserId);
VaultNudgeType.newLoginItemStatus,
component.activeUserId,
);
}); });
}); });

View File

@@ -9,7 +9,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/sdk-internal"; import { CipherType } from "@bitwarden/sdk-internal";
import { SpotlightComponent } from "../../../components/spotlight/spotlight.component"; import { SpotlightComponent } from "../../../components/spotlight/spotlight.component";
import { VaultNudgesService, VaultNudgeType } from "../../../services/vault-nudges.service"; import { NudgesService, NudgeType } from "../../../services/nudges.service";
@Component({ @Component({
selector: "vault-new-item-nudge", selector: "vault-new-item-nudge",
@@ -23,12 +23,12 @@ export class NewItemNudgeComponent implements OnInit {
showNewItemSpotlight: boolean = false; showNewItemSpotlight: boolean = false;
nudgeTitle: string = ""; nudgeTitle: string = "";
nudgeBody: string = ""; nudgeBody: string = "";
dismissalNudgeType: VaultNudgeType | null = null; dismissalNudgeType: NudgeType | null = null;
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private accountService: AccountService, private accountService: AccountService,
private vaultNudgesService: VaultNudgesService, private nudgesService: NudgesService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -39,25 +39,25 @@ export class NewItemNudgeComponent implements OnInit {
const nudgeBodyOne = this.i18nService.t("newLoginNudgeBodyOne"); const nudgeBodyOne = this.i18nService.t("newLoginNudgeBodyOne");
const nudgeBodyBold = this.i18nService.t("newLoginNudgeBodyBold"); const nudgeBodyBold = this.i18nService.t("newLoginNudgeBodyBold");
const nudgeBodyTwo = this.i18nService.t("newLoginNudgeBodyTwo"); const nudgeBodyTwo = this.i18nService.t("newLoginNudgeBodyTwo");
this.dismissalNudgeType = VaultNudgeType.newLoginItemStatus; this.dismissalNudgeType = NudgeType.NewLoginItemStatus;
this.nudgeTitle = this.i18nService.t("newLoginNudgeTitle"); this.nudgeTitle = this.i18nService.t("newLoginNudgeTitle");
this.nudgeBody = `${nudgeBodyOne} <strong>${nudgeBodyBold}</strong> ${nudgeBodyTwo}`; this.nudgeBody = `${nudgeBodyOne} <strong>${nudgeBodyBold}</strong> ${nudgeBodyTwo}`;
break; break;
} }
case CipherType.Card: case CipherType.Card:
this.dismissalNudgeType = VaultNudgeType.newCardItemStatus; this.dismissalNudgeType = NudgeType.NewCardItemStatus;
this.nudgeTitle = this.i18nService.t("newCardNudgeTitle"); this.nudgeTitle = this.i18nService.t("newCardNudgeTitle");
this.nudgeBody = this.i18nService.t("newCardNudgeBody"); this.nudgeBody = this.i18nService.t("newCardNudgeBody");
break; break;
case CipherType.Identity: case CipherType.Identity:
this.dismissalNudgeType = VaultNudgeType.newIdentityItemStatus; this.dismissalNudgeType = NudgeType.NewIdentityItemStatus;
this.nudgeTitle = this.i18nService.t("newIdentityNudgeTitle"); this.nudgeTitle = this.i18nService.t("newIdentityNudgeTitle");
this.nudgeBody = this.i18nService.t("newIdentityNudgeBody"); this.nudgeBody = this.i18nService.t("newIdentityNudgeBody");
break; break;
case CipherType.SecureNote: case CipherType.SecureNote:
this.dismissalNudgeType = VaultNudgeType.newNoteItemStatus; this.dismissalNudgeType = NudgeType.NewNoteItemStatus;
this.nudgeTitle = this.i18nService.t("newNoteNudgeTitle"); this.nudgeTitle = this.i18nService.t("newNoteNudgeTitle");
this.nudgeBody = this.i18nService.t("newNoteNudgeBody"); this.nudgeBody = this.i18nService.t("newNoteNudgeBody");
break; break;
@@ -66,7 +66,7 @@ export class NewItemNudgeComponent implements OnInit {
const sshPartOne = this.i18nService.t("newSshNudgeBodyOne"); const sshPartOne = this.i18nService.t("newSshNudgeBodyOne");
const sshPartTwo = this.i18nService.t("newSshNudgeBodyTwo"); const sshPartTwo = this.i18nService.t("newSshNudgeBodyTwo");
this.dismissalNudgeType = VaultNudgeType.newSshItemStatus; this.dismissalNudgeType = NudgeType.NewSshItemStatus;
this.nudgeTitle = this.i18nService.t("newSshNudgeTitle"); this.nudgeTitle = this.i18nService.t("newSshNudgeTitle");
this.nudgeBody = `${sshPartOne} <a href="https://bitwarden.com/help/ssh-agent" class="tw-text-primary-600 tw-font-bold" target="_blank">${sshPartTwo}</a>`; this.nudgeBody = `${sshPartOne} <a href="https://bitwarden.com/help/ssh-agent" class="tw-text-primary-600 tw-font-bold" target="_blank">${sshPartTwo}</a>`;
break; break;
@@ -75,23 +75,19 @@ export class NewItemNudgeComponent implements OnInit {
throw new Error("Unsupported cipher type"); throw new Error("Unsupported cipher type");
} }
this.showNewItemSpotlight = await this.checkHasSpotlightDismissed( this.showNewItemSpotlight = await this.checkHasSpotlightDismissed(
this.dismissalNudgeType as VaultNudgeType, this.dismissalNudgeType as NudgeType,
this.activeUserId, this.activeUserId,
); );
} }
async dismissNewItemSpotlight() { async dismissNewItemSpotlight() {
if (this.dismissalNudgeType && this.activeUserId) { if (this.dismissalNudgeType && this.activeUserId) {
await this.vaultNudgesService.dismissNudge( await this.nudgesService.dismissNudge(this.dismissalNudgeType, this.activeUserId as UserId);
this.dismissalNudgeType,
this.activeUserId as UserId,
);
this.showNewItemSpotlight = false; this.showNewItemSpotlight = false;
} }
} }
async checkHasSpotlightDismissed(nudgeType: VaultNudgeType, userId: UserId): Promise<boolean> { async checkHasSpotlightDismissed(nudgeType: NudgeType, userId: UserId): Promise<boolean> {
return !(await firstValueFrom(this.vaultNudgesService.showNudge$(nudgeType, userId))) return await firstValueFrom(this.nudgesService.showNudgeSpotlight$(nudgeType, userId));
.hasSpotlightDismissed;
} }
} }

View File

@@ -21,7 +21,7 @@ export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.compon
export * from "./components/carousel"; export * from "./components/carousel";
export * as VaultIcons from "./icons"; export * as VaultIcons from "./icons";
export * from "./services/vault-nudges.service"; export * from "./services/nudges.service";
export * from "./services/custom-nudges-services"; export * from "./services/custom-nudges-services";
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";

View File

@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { DefaultSingleNudgeService } from "../default-single-nudge.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service"; import { NudgeStatus, NudgeType } from "../nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
@@ -21,7 +21,7 @@ export class AutofillNudgeService extends DefaultSingleNudgeService {
vaultProfileService = inject(VaultProfileService); vaultProfileService = inject(VaultProfileService);
logService = inject(LogService); logService = inject(LogService);
nudgeStatus$(_: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { nudgeStatus$(_: NudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe( const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => { catchError(() => {
this.logService.error("Error getting profile creation date"); this.logService.error("Error getting profile creation date");
@@ -32,7 +32,7 @@ export class AutofillNudgeService extends DefaultSingleNudgeService {
return combineLatest([ return combineLatest([
profileDate$, profileDate$,
this.getNudgeStatus$(VaultNudgeType.AutofillNudge, userId), this.getNudgeStatus$(NudgeType.AutofillNudge, userId),
of(Date.now() - THIRTY_DAYS_MS), of(Date.now() - THIRTY_DAYS_MS),
]).pipe( ]).pipe(
map(([profileCreationDate, status, profileCutoff]) => { map(([profileCreationDate, status, profileCutoff]) => {

View File

@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { DefaultSingleNudgeService } from "../default-single-nudge.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service"; import { NudgeStatus, NudgeType } from "../nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
@@ -16,7 +16,7 @@ export class DownloadBitwardenNudgeService extends DefaultSingleNudgeService {
private vaultProfileService = inject(VaultProfileService); private vaultProfileService = inject(VaultProfileService);
private logService = inject(LogService); private logService = inject(LogService);
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe( const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => { catchError(() => {
this.logService.error("Failed to load profile date:"); this.logService.error("Failed to load profile date:");

View File

@@ -7,7 +7,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DefaultSingleNudgeService } from "../default-single-nudge.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service"; import { NudgeStatus, NudgeType } from "../nudges.service";
/** /**
* Custom Nudge Service Checking Nudge Status For Empty Vault * Custom Nudge Service Checking Nudge Status For Empty Vault
@@ -20,7 +20,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
organizationService = inject(OrganizationService); organizationService = inject(OrganizationService);
collectionService = inject(CollectionService); collectionService = inject(CollectionService);
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
return combineLatest([ return combineLatest([
this.getNudgeStatus$(nudgeType, userId), this.getNudgeStatus$(nudgeType, userId),
this.cipherService.cipherViews$(userId), this.cipherService.cipherViews$(userId),

View File

@@ -8,7 +8,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DefaultSingleNudgeService } from "../default-single-nudge.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service"; import { NudgeStatus, NudgeType } from "../nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
@@ -23,7 +23,7 @@ export class HasItemsNudgeService extends DefaultSingleNudgeService {
vaultProfileService = inject(VaultProfileService); vaultProfileService = inject(VaultProfileService);
logService = inject(LogService); logService = inject(LogService);
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe( const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => { catchError(() => {
this.logService.error("Error getting profile creation date"); this.logService.error("Error getting profile creation date");
@@ -51,7 +51,7 @@ export class HasItemsNudgeService extends DefaultSingleNudgeService {
}; };
// permanently dismiss both the Empty Vault Nudge and Has Items Vault Nudge if the profile is older than 30 days // permanently dismiss both the Empty Vault Nudge and Has Items Vault Nudge if the profile is older than 30 days
await this.setNudgeStatus(nudgeType, dismissedStatus, userId); await this.setNudgeStatus(nudgeType, dismissedStatus, userId);
await this.setNudgeStatus(VaultNudgeType.EmptyVaultNudge, dismissedStatus, userId); await this.setNudgeStatus(NudgeType.EmptyVaultNudge, dismissedStatus, userId);
return dismissedStatus; return dismissedStatus;
} else if (nudgeStatus.hasSpotlightDismissed) { } else if (nudgeStatus.hasSpotlightDismissed) {
return nudgeStatus; return nudgeStatus;

View File

@@ -6,7 +6,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { DefaultSingleNudgeService } from "../default-single-nudge.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service"; import { NudgeStatus, NudgeType } from "../nudges.service";
/** /**
* Custom Nudge Service Checking Nudge Status For Vault New Item Types * Custom Nudge Service Checking Nudge Status For Vault New Item Types
@@ -17,7 +17,7 @@ import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
export class NewItemNudgeService extends DefaultSingleNudgeService { export class NewItemNudgeService extends DefaultSingleNudgeService {
cipherService = inject(CipherService); cipherService = inject(CipherService);
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
return combineLatest([ return combineLatest([
this.getNudgeStatus$(nudgeType, userId), this.getNudgeStatus$(nudgeType, userId),
this.cipherService.cipherViews$(userId), this.cipherService.cipherViews$(userId),
@@ -30,19 +30,19 @@ export class NewItemNudgeService extends DefaultSingleNudgeService {
let currentType: CipherType; let currentType: CipherType;
switch (nudgeType) { switch (nudgeType) {
case VaultNudgeType.newLoginItemStatus: case NudgeType.NewLoginItemStatus:
currentType = CipherType.Login; currentType = CipherType.Login;
break; break;
case VaultNudgeType.newCardItemStatus: case NudgeType.NewCardItemStatus:
currentType = CipherType.Card; currentType = CipherType.Card;
break; break;
case VaultNudgeType.newIdentityItemStatus: case NudgeType.NewIdentityItemStatus:
currentType = CipherType.Identity; currentType = CipherType.Identity;
break; break;
case VaultNudgeType.newNoteItemStatus: case NudgeType.NewNoteItemStatus:
currentType = CipherType.SecureNote; currentType = CipherType.SecureNote;
break; break;
case VaultNudgeType.newSshItemStatus: case NudgeType.NewSshItemStatus:
currentType = CipherType.SshKey; currentType = CipherType.SshKey;
break; break;
} }

View File

@@ -4,19 +4,15 @@ import { map, Observable } from "rxjs";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { import { NudgeStatus, NUDGE_DISMISSED_DISK_KEY, NudgeType } from "./nudges.service";
NudgeStatus,
VAULT_NUDGE_DISMISSED_DISK_KEY,
VaultNudgeType,
} from "./vault-nudges.service";
/** /**
* Base interface for handling a nudge's status * Base interface for handling a nudge's status
*/ */
export interface SingleNudgeService { export interface SingleNudgeService {
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus>; nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus>;
setNudgeStatus(nudgeType: VaultNudgeType, newStatus: NudgeStatus, userId: UserId): Promise<void>; setNudgeStatus(nudgeType: NudgeType, newStatus: NudgeStatus, userId: UserId): Promise<void>;
} }
/** /**
@@ -28,9 +24,9 @@ export interface SingleNudgeService {
export class DefaultSingleNudgeService implements SingleNudgeService { export class DefaultSingleNudgeService implements SingleNudgeService {
stateProvider = inject(StateProvider); stateProvider = inject(StateProvider);
protected getNudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { protected getNudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
return this.stateProvider return this.stateProvider
.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY) .getUser(userId, NUDGE_DISMISSED_DISK_KEY)
.state$.pipe( .state$.pipe(
map( map(
(nudges) => (nudges) =>
@@ -39,16 +35,12 @@ export class DefaultSingleNudgeService implements SingleNudgeService {
); );
} }
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> { nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
return this.getNudgeStatus$(nudgeType, userId); return this.getNudgeStatus$(nudgeType, userId);
} }
async setNudgeStatus( async setNudgeStatus(nudgeType: NudgeType, status: NudgeStatus, userId: UserId): Promise<void> {
nudgeType: VaultNudgeType, await this.stateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
status: NudgeStatus,
userId: UserId,
): Promise<void> {
await this.stateProvider.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
nudges ??= {}; nudges ??= {};
nudges[nudgeType] = status; nudges[nudgeType] = status;
return nudges; return nudges;

View File

@@ -18,7 +18,7 @@ import {
DownloadBitwardenNudgeService, DownloadBitwardenNudgeService,
} from "./custom-nudges-services"; } from "./custom-nudges-services";
import { DefaultSingleNudgeService } from "./default-single-nudge.service"; import { DefaultSingleNudgeService } from "./default-single-nudge.service";
import { VaultNudgesService, VaultNudgeType } from "./vault-nudges.service"; import { NudgesService, NudgeType } from "./nudges.service";
describe("Vault Nudges Service", () => { describe("Vault Nudges Service", () => {
let fakeStateProvider: FakeStateProvider; let fakeStateProvider: FakeStateProvider;
@@ -29,7 +29,7 @@ describe("Vault Nudges Service", () => {
getFeatureFlag: jest.fn().mockReturnValue(true), getFeatureFlag: jest.fn().mockReturnValue(true),
}; };
const vaultNudgeServices = [EmptyVaultNudgeService, DownloadBitwardenNudgeService]; const nudgeServices = [EmptyVaultNudgeService, DownloadBitwardenNudgeService];
beforeEach(async () => { beforeEach(async () => {
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
@@ -38,7 +38,7 @@ describe("Vault Nudges Service", () => {
imports: [], imports: [],
providers: [ providers: [
{ {
provide: VaultNudgesService, provide: NudgesService,
}, },
{ {
provide: DefaultSingleNudgeService, provide: DefaultSingleNudgeService,
@@ -83,13 +83,13 @@ describe("Vault Nudges Service", () => {
const service = testBed.inject(DefaultSingleNudgeService); const service = testBed.inject(DefaultSingleNudgeService);
await service.setNudgeStatus( await service.setNudgeStatus(
VaultNudgeType.EmptyVaultNudge, NudgeType.EmptyVaultNudge,
{ hasBadgeDismissed: true, hasSpotlightDismissed: true }, { hasBadgeDismissed: true, hasSpotlightDismissed: true },
"user-id" as UserId, "user-id" as UserId,
); );
const result = await firstValueFrom( const result = await firstValueFrom(
service.nudgeStatus$(VaultNudgeType.EmptyVaultNudge, "user-id" as UserId), service.nudgeStatus$(NudgeType.EmptyVaultNudge, "user-id" as UserId),
); );
expect(result).toEqual({ hasBadgeDismissed: true, hasSpotlightDismissed: true }); expect(result).toEqual({ hasBadgeDismissed: true, hasSpotlightDismissed: true });
}); });
@@ -98,27 +98,27 @@ describe("Vault Nudges Service", () => {
const service = testBed.inject(DefaultSingleNudgeService); const service = testBed.inject(DefaultSingleNudgeService);
await service.setNudgeStatus( await service.setNudgeStatus(
VaultNudgeType.EmptyVaultNudge, NudgeType.EmptyVaultNudge,
{ hasBadgeDismissed: false, hasSpotlightDismissed: false }, { hasBadgeDismissed: false, hasSpotlightDismissed: false },
"user-id" as UserId, "user-id" as UserId,
); );
const result = await firstValueFrom( const result = await firstValueFrom(
service.nudgeStatus$(VaultNudgeType.EmptyVaultNudge, "user-id" as UserId), service.nudgeStatus$(NudgeType.EmptyVaultNudge, "user-id" as UserId),
); );
expect(result).toEqual({ hasBadgeDismissed: false, hasSpotlightDismissed: false }); expect(result).toEqual({ hasBadgeDismissed: false, hasSpotlightDismissed: false });
}); });
}); });
describe("VaultNudgesService", () => { describe("NudgesService", () => {
it("should return true, the proper value from the custom nudge service nudgeStatus$", async () => { it("should return true, the proper value from the custom nudge service nudgeStatus$", async () => {
TestBed.overrideProvider(HasItemsNudgeService, { TestBed.overrideProvider(HasItemsNudgeService, {
useValue: { nudgeStatus$: () => of(true) }, useValue: { nudgeStatus$: () => of(true) },
}); });
const service = testBed.inject(VaultNudgesService); const service = testBed.inject(NudgesService);
const result = await firstValueFrom( const result = await firstValueFrom(
service.showNudge$(VaultNudgeType.HasVaultItems, "user-id" as UserId), service.showNudgeStatus$(NudgeType.HasVaultItems, "user-id" as UserId),
); );
expect(result).toBe(true); expect(result).toBe(true);
@@ -128,10 +128,40 @@ describe("Vault Nudges Service", () => {
TestBed.overrideProvider(HasItemsNudgeService, { TestBed.overrideProvider(HasItemsNudgeService, {
useValue: { nudgeStatus$: () => of(false) }, useValue: { nudgeStatus$: () => of(false) },
}); });
const service = testBed.inject(VaultNudgesService); const service = testBed.inject(NudgesService);
const result = await firstValueFrom( const result = await firstValueFrom(
service.showNudge$(VaultNudgeType.HasVaultItems, "user-id" as UserId), service.showNudgeStatus$(NudgeType.HasVaultItems, "user-id" as UserId),
);
expect(result).toBe(false);
});
it("should return showNudgeSpotlight$ false if hasSpotLightDismissed is true", async () => {
TestBed.overrideProvider(HasItemsNudgeService, {
useValue: {
nudgeStatus$: () => of({ hasSpotlightDismissed: true, hasBadgeDismissed: true }),
},
});
const service = testBed.inject(NudgesService);
const result = await firstValueFrom(
service.showNudgeSpotlight$(NudgeType.HasVaultItems, "user-id" as UserId),
);
expect(result).toBe(false);
});
it("should return showNudgeBadge$ false when hasBadgeDismissed is true", async () => {
TestBed.overrideProvider(HasItemsNudgeService, {
useValue: {
nudgeStatus$: () => of({ hasSpotlightDismissed: true, hasBadgeDismissed: true }),
},
});
const service = testBed.inject(NudgesService);
const result = await firstValueFrom(
service.showNudgeBadge$(NudgeType.HasVaultItems, "user-id" as UserId),
); );
expect(result).toBe(false); expect(result).toBe(false);
@@ -140,7 +170,7 @@ describe("Vault Nudges Service", () => {
describe("HasActiveBadges", () => { describe("HasActiveBadges", () => {
it("should return true if a nudgeType with hasBadgeDismissed === false", async () => { it("should return true if a nudgeType with hasBadgeDismissed === false", async () => {
vaultNudgeServices.forEach((service) => { nudgeServices.forEach((service) => {
TestBed.overrideProvider(service, { TestBed.overrideProvider(service, {
useValue: { useValue: {
nudgeStatus$: () => of({ hasBadgeDismissed: false, hasSpotlightDismissed: false }), nudgeStatus$: () => of({ hasBadgeDismissed: false, hasSpotlightDismissed: false }),
@@ -148,21 +178,21 @@ describe("Vault Nudges Service", () => {
}); });
}); });
const service = testBed.inject(VaultNudgesService); const service = testBed.inject(NudgesService);
const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId)); const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId));
expect(result).toBe(true); expect(result).toBe(true);
}); });
it("should return false if all nudgeTypes have hasBadgeDismissed === true", async () => { it("should return false if all nudgeTypes have hasBadgeDismissed === true", async () => {
vaultNudgeServices.forEach((service) => { nudgeServices.forEach((service) => {
TestBed.overrideProvider(service, { TestBed.overrideProvider(service, {
useValue: { useValue: {
nudgeStatus$: () => of({ hasBadgeDismissed: true, hasSpotlightDismissed: false }), nudgeStatus$: () => of({ hasBadgeDismissed: true, hasSpotlightDismissed: false }),
}, },
}); });
}); });
const service = testBed.inject(VaultNudgesService); const service = testBed.inject(NudgesService);
const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId)); const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId));

View File

@@ -3,7 +3,7 @@ import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserKeyDefinition, VAULT_NUDGES_DISK } from "@bitwarden/common/platform/state"; import { UserKeyDefinition, NUDGES_DISK } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { import {
@@ -25,7 +25,7 @@ export type NudgeStatus = {
*/ */
// FIXME: update to use a const object instead of a typescript enum // FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums // eslint-disable-next-line @bitwarden/platform/no-enums
export enum VaultNudgeType { export enum NudgeType {
/** Nudge to show when user has no items in their vault /** Nudge to show when user has no items in their vault
* Add future nudges here * Add future nudges here
*/ */
@@ -33,16 +33,16 @@ export enum VaultNudgeType {
HasVaultItems = "has-vault-items", HasVaultItems = "has-vault-items",
AutofillNudge = "autofill-nudge", AutofillNudge = "autofill-nudge",
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",
newIdentityItemStatus = "new-identity-item-status", NewIdentityItemStatus = "new-identity-item-status",
newNoteItemStatus = "new-note-item-status", NewNoteItemStatus = "new-note-item-status",
newSshItemStatus = "new-ssh-item-status", NewSshItemStatus = "new-ssh-item-status",
} }
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition< export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
Partial<Record<VaultNudgeType, NudgeStatus>> Partial<Record<NudgeType, NudgeStatus>>
>(VAULT_NUDGES_DISK, "vaultNudgeDismissed", { >(NUDGES_DISK, "vaultNudgeDismissed", {
deserializer: (nudge) => nudge, deserializer: (nudge) => nudge,
clearOn: [], // Do not clear dismissals clearOn: [], // Do not clear dismissals
}); });
@@ -50,7 +50,7 @@ export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
export class VaultNudgesService { export class NudgesService {
private newItemNudgeService = inject(NewItemNudgeService); private newItemNudgeService = inject(NewItemNudgeService);
/** /**
@@ -58,16 +58,16 @@ export class VaultNudgesService {
* Each nudge type can have its own service to determine when to show the nudge * Each nudge type can have its own service to determine when to show the nudge
* @private * @private
*/ */
private customNudgeServices: Partial<Record<VaultNudgeType, SingleNudgeService>> = { private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService), [NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService), [NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
[VaultNudgeType.AutofillNudge]: inject(AutofillNudgeService), [NudgeType.AutofillNudge]: inject(AutofillNudgeService),
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService), [NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService, [NudgeType.NewLoginItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService, [NudgeType.NewCardItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService, [NudgeType.NewIdentityItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newNoteItemStatus]: this.newItemNudgeService, [NudgeType.NewNoteItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newSshItemStatus]: this.newItemNudgeService, [NudgeType.NewSshItemStatus]: this.newItemNudgeService,
}; };
/** /**
@@ -78,16 +78,52 @@ export class VaultNudgesService {
private defaultNudgeService = inject(DefaultSingleNudgeService); private defaultNudgeService = inject(DefaultSingleNudgeService);
private configService = inject(ConfigService); private configService = inject(ConfigService);
private getNudgeService(nudge: VaultNudgeType): SingleNudgeService { private getNudgeService(nudge: NudgeType): SingleNudgeService {
return this.customNudgeServices[nudge] ?? this.defaultNudgeService; return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
} }
/**
* Check if a nudge Spotlight should be shown to the user
* @param nudge
* @param userId
*/
showNudgeSpotlight$(nudge: NudgeType, userId: UserId): Observable<boolean> {
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
switchMap((hasVaultNudgeFlag) => {
if (!hasVaultNudgeFlag) {
return of(false);
}
return this.getNudgeService(nudge)
.nudgeStatus$(nudge, userId)
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed));
}),
);
}
/**
* Check if a nudge Badge should be shown to the user
* @param nudge
* @param userId
*/
showNudgeBadge$(nudge: NudgeType, userId: UserId): Observable<boolean> {
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
switchMap((hasVaultNudgeFlag) => {
if (!hasVaultNudgeFlag) {
return of(false);
}
return this.getNudgeService(nudge)
.nudgeStatus$(nudge, userId)
.pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed));
}),
);
}
/** /**
* Check if a nudge should be shown to the user * Check if a nudge should be shown to the user
* @param nudge * @param nudge
* @param userId * @param userId
*/ */
showNudge$(nudge: VaultNudgeType, userId: UserId) { showNudgeStatus$(nudge: NudgeType, userId: UserId) {
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe( return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
switchMap((hasVaultNudgeFlag) => { switchMap((hasVaultNudgeFlag) => {
if (!hasVaultNudgeFlag) { if (!hasVaultNudgeFlag) {
@@ -103,7 +139,7 @@ export class VaultNudgesService {
* @param nudge * @param nudge
* @param userId * @param userId
*/ */
async dismissNudge(nudge: VaultNudgeType, userId: UserId, onlyBadge: boolean = false) { async dismissNudge(nudge: NudgeType, userId: UserId, onlyBadge: boolean = false) {
const dismissedStatus = onlyBadge const dismissedStatus = onlyBadge
? { hasBadgeDismissed: true, hasSpotlightDismissed: false } ? { hasBadgeDismissed: true, hasSpotlightDismissed: false }
: { hasBadgeDismissed: true, hasSpotlightDismissed: true }; : { hasBadgeDismissed: true, hasSpotlightDismissed: true };
@@ -116,7 +152,7 @@ export class VaultNudgesService {
*/ */
hasActiveBadges$(userId: UserId): Observable<boolean> { hasActiveBadges$(userId: UserId): Observable<boolean> {
// Add more nudge types here if they have the settings badge feature // Add more nudge types here if they have the settings badge feature
const nudgeTypes = [VaultNudgeType.EmptyVaultNudge, VaultNudgeType.DownloadBitwarden]; const nudgeTypes = [NudgeType.EmptyVaultNudge, NudgeType.DownloadBitwarden];
const nudgeTypesWithBadge$ = nudgeTypes.map((nudge) => { const nudgeTypesWithBadge$ = nudgeTypes.map((nudge) => {
return this.getNudgeService(nudge) return this.getNudgeService(nudge)