mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-18803] nudges new items (#14523)
* Added new-items-nudge service and component to show spotlight for new item nudges
This commit is contained in:
@@ -5248,5 +5248,35 @@
|
|||||||
},
|
},
|
||||||
"hasItemsVaultNudgeBody": {
|
"hasItemsVaultNudgeBody": {
|
||||||
"message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else"
|
"message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else"
|
||||||
|
},
|
||||||
|
"newLoginNudgeTitle": {
|
||||||
|
"message": "Save time with autofill"
|
||||||
|
},
|
||||||
|
"newLoginNudgeBody": {
|
||||||
|
"message": "Include a Website so this login appears as an autofill suggestion."
|
||||||
|
},
|
||||||
|
"newCardNudgeTitle": {
|
||||||
|
"message": "Seamless online checkout"
|
||||||
|
},
|
||||||
|
"newCardNudgeBody": {
|
||||||
|
"message": "With cards, easily autofill payment forms securely and accurately."
|
||||||
|
},
|
||||||
|
"newIdentityNudgeTitle": {
|
||||||
|
"message": "Simplify creating accounts"
|
||||||
|
},
|
||||||
|
"newIdentityNudgeBody": {
|
||||||
|
"message": "With identities, quickly autofill long registration or contact forms."
|
||||||
|
},
|
||||||
|
"newNoteNudgeTitle": {
|
||||||
|
"message": "Keep your sensitive data safe"
|
||||||
|
},
|
||||||
|
"newNoteNudgeBody": {
|
||||||
|
"message": "With notes, securely store sensitive data like banking or insurance details."
|
||||||
|
},
|
||||||
|
"newSshNudgeTitle": {
|
||||||
|
"message": "Developer-friendly SSH access"
|
||||||
|
},
|
||||||
|
"newSshNudgeBody": {
|
||||||
|
"message": "Store your keys and connect with the SSH agent for fast, encrypted authentication."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3712,5 +3712,35 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"message": "Move"
|
"message": "Move"
|
||||||
|
},
|
||||||
|
"newLoginNudgeTitle": {
|
||||||
|
"message": "Save time with autofill"
|
||||||
|
},
|
||||||
|
"newLoginNudgeBody": {
|
||||||
|
"message": "Include a Website so this login appears as an autofill suggestion."
|
||||||
|
},
|
||||||
|
"newCardNudgeTitle": {
|
||||||
|
"message": "Seamless online checkout"
|
||||||
|
},
|
||||||
|
"newCardNudgeBody": {
|
||||||
|
"message": "With cards, easily autofill payment forms securely and accurately."
|
||||||
|
},
|
||||||
|
"newIdentityNudgeTitle": {
|
||||||
|
"message": "Simplify creating accounts"
|
||||||
|
},
|
||||||
|
"newIdentityNudgeBody": {
|
||||||
|
"message": "With identities, quickly autofill long registration or contact forms."
|
||||||
|
},
|
||||||
|
"newNoteNudgeTitle": {
|
||||||
|
"message": "Keep your sensitive data safe"
|
||||||
|
},
|
||||||
|
"newNoteNudgeBody": {
|
||||||
|
"message": "With notes, securely store sensitive data like banking or insurance details."
|
||||||
|
},
|
||||||
|
"newSshNudgeTitle": {
|
||||||
|
"message": "Developer-friendly SSH access"
|
||||||
|
},
|
||||||
|
"newSshNudgeBody": {
|
||||||
|
"message": "Store your keys and connect with the SSH agent for fast, encrypted authentication."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10629,6 +10629,36 @@
|
|||||||
"newBusinessUnit": {
|
"newBusinessUnit": {
|
||||||
"message": "New business unit"
|
"message": "New business unit"
|
||||||
},
|
},
|
||||||
|
"newLoginNudgeTitle": {
|
||||||
|
"message": "Save time with autofill"
|
||||||
|
},
|
||||||
|
"newLoginNudgeBody": {
|
||||||
|
"message": "Include a Website so this login appears as an autofill suggestion."
|
||||||
|
},
|
||||||
|
"newCardNudgeTitle": {
|
||||||
|
"message": "Seamless online checkout"
|
||||||
|
},
|
||||||
|
"newCardNudgeBody": {
|
||||||
|
"message": "With cards, easily autofill payment forms securely and accurately."
|
||||||
|
},
|
||||||
|
"newIdentityNudgeTitle": {
|
||||||
|
"message": "Simplify creating accounts"
|
||||||
|
},
|
||||||
|
"newIdentityNudgeBody": {
|
||||||
|
"message": "With identities, quickly autofill long registration or contact forms."
|
||||||
|
},
|
||||||
|
"newNoteNudgeTitle": {
|
||||||
|
"message": "Keep your sensitive data safe"
|
||||||
|
},
|
||||||
|
"newNoteNudgeBody": {
|
||||||
|
"message": "With notes, securely store sensitive data like banking or insurance details."
|
||||||
|
},
|
||||||
|
"newSshNudgeTitle": {
|
||||||
|
"message": "Developer-friendly SSH access"
|
||||||
|
},
|
||||||
|
"newSshNudgeBody": {
|
||||||
|
"message": "Store your keys and connect with the SSH agent for fast, encrypted authentication."
|
||||||
|
},
|
||||||
"restart": {
|
"restart": {
|
||||||
"message": "Restart"
|
"message": "Restart"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -206,7 +206,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");
|
export const VAULT_NUDGES_DISK = new StateDefinition("vaultNudges", "disk", { web: "disk-local" });
|
||||||
export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
|
export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
|
||||||
"vaultBrowserIntroCarousel",
|
"vaultBrowserIntroCarousel",
|
||||||
"disk",
|
"disk",
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ import { AsyncActionsModule, ButtonModule, ItemModule, ToastService } from "@bit
|
|||||||
import {
|
import {
|
||||||
CipherFormConfig,
|
CipherFormConfig,
|
||||||
CipherFormGenerationService,
|
CipherFormGenerationService,
|
||||||
|
NudgeStatus,
|
||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
|
VaultNudgesService,
|
||||||
} 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
|
||||||
@@ -47,6 +49,7 @@ import { CipherFormService } from "./abstractions/cipher-form.service";
|
|||||||
import { TotpCaptureService } from "./abstractions/totp-capture.service";
|
import { TotpCaptureService } from "./abstractions/totp-capture.service";
|
||||||
import { CipherFormModule } from "./cipher-form.module";
|
import { CipherFormModule } from "./cipher-form.module";
|
||||||
import { CipherFormComponent } from "./components/cipher-form.component";
|
import { CipherFormComponent } from "./components/cipher-form.component";
|
||||||
|
import { NewItemNudgeComponent } from "./components/new-item-nudge/new-item-nudge.component";
|
||||||
import { CipherFormCacheService } from "./services/default-cipher-form-cache.service";
|
import { CipherFormCacheService } from "./services/default-cipher-form-cache.service";
|
||||||
|
|
||||||
const defaultConfig: CipherFormConfig = {
|
const defaultConfig: CipherFormConfig = {
|
||||||
@@ -132,8 +135,23 @@ export default {
|
|||||||
component: CipherFormComponent,
|
component: CipherFormComponent,
|
||||||
decorators: [
|
decorators: [
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [CipherFormModule, AsyncActionsModule, ButtonModule, ItemModule],
|
imports: [
|
||||||
|
CipherFormModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
NewItemNudgeComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultNudgesService,
|
||||||
|
useValue: {
|
||||||
|
showNudge$: new BehaviorSubject({
|
||||||
|
hasBadgeDismissed: true,
|
||||||
|
hasSpotlightDismissed: true,
|
||||||
|
} as NudgeStatus),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: CipherFormService,
|
provide: CipherFormService,
|
||||||
useClass: TestAddEditFormService,
|
useClass: TestAddEditFormService,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<vault-new-item-nudge *ngIf="!loading" [configType]="config.cipherType"> </vault-new-item-nudge>
|
||||||
<form [id]="formId" [formGroup]="cipherForm" [bitSubmit]="submit">
|
<form [id]="formId" [formGroup]="cipherForm" [bitSubmit]="submit">
|
||||||
<!-- TODO: Should we show a loading spinner here? Or emit a ready event for the container to handle loading state -->
|
<!-- TODO: Should we show a loading spinner here? Or emit a ready event for the container to handle loading state -->
|
||||||
<ng-container *ngIf="!loading">
|
<ng-container *ngIf="!loading">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { CardDetailsSectionComponent } from "./card-details-section/card-details
|
|||||||
import { IdentitySectionComponent } from "./identity/identity.component";
|
import { IdentitySectionComponent } from "./identity/identity.component";
|
||||||
import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component";
|
import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component";
|
||||||
import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component";
|
import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component";
|
||||||
|
import { NewItemNudgeComponent } from "./new-item-nudge/new-item-nudge.component";
|
||||||
import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.component";
|
import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -76,6 +77,7 @@ import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.componen
|
|||||||
NgIf,
|
NgIf,
|
||||||
AdditionalOptionsSectionComponent,
|
AdditionalOptionsSectionComponent,
|
||||||
LoginDetailsSectionComponent,
|
LoginDetailsSectionComponent,
|
||||||
|
NewItemNudgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer {
|
export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<ng-container *ngIf="showNewItemSpotlight">
|
||||||
|
<bit-spotlight
|
||||||
|
[title]="nudgeTitle"
|
||||||
|
[subtitle]="nudgeBody"
|
||||||
|
(onDismiss)="dismissNewItemSpotlight()"
|
||||||
|
>
|
||||||
|
</bit-spotlight>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherType } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { VaultNudgesService, VaultNudgeType } from "../../../services/vault-nudges.service";
|
||||||
|
|
||||||
|
import { NewItemNudgeComponent } from "./new-item-nudge.component";
|
||||||
|
|
||||||
|
describe("NewItemNudgeComponent", () => {
|
||||||
|
let component: NewItemNudgeComponent;
|
||||||
|
let fixture: ComponentFixture<NewItemNudgeComponent>;
|
||||||
|
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let accountService: MockProxy<AccountService>;
|
||||||
|
let vaultNudgesService: MockProxy<VaultNudgesService>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
i18nService = mock<I18nService>({ t: (key: string) => key });
|
||||||
|
accountService = mock<AccountService>();
|
||||||
|
vaultNudgesService = mock<VaultNudgesService>();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [NewItemNudgeComponent, CommonModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: I18nService, useValue: i18nService },
|
||||||
|
{ provide: AccountService, useValue: accountService },
|
||||||
|
{ provide: VaultNudgesService, useValue: vaultNudgesService },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NewItemNudgeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.configType = null; // Set to null for initial state
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set nudge title and body for CipherType.Login type", async () => {
|
||||||
|
component.configType = CipherType.Login;
|
||||||
|
accountService.activeAccount$ = of({ id: "test-user-id" as UserId } as Account);
|
||||||
|
jest.spyOn(component, "checkHasSpotlightDismissed").mockResolvedValue(true);
|
||||||
|
|
||||||
|
await component.ngOnInit();
|
||||||
|
|
||||||
|
expect(component.showNewItemSpotlight).toBe(true);
|
||||||
|
expect(component.nudgeTitle).toBe("newLoginNudgeTitle");
|
||||||
|
expect(component.nudgeBody).toBe("newLoginNudgeBody");
|
||||||
|
expect(component.dismissalNudgeType).toBe(VaultNudgeType.newLoginItemStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set nudge title and body for CipherType.Card type", async () => {
|
||||||
|
component.configType = CipherType.Card;
|
||||||
|
accountService.activeAccount$ = of({ id: "test-user-id" as UserId } as Account);
|
||||||
|
jest.spyOn(component, "checkHasSpotlightDismissed").mockResolvedValue(true);
|
||||||
|
|
||||||
|
await component.ngOnInit();
|
||||||
|
|
||||||
|
expect(component.showNewItemSpotlight).toBe(true);
|
||||||
|
expect(component.nudgeTitle).toBe("newCardNudgeTitle");
|
||||||
|
expect(component.nudgeBody).toBe("newCardNudgeBody");
|
||||||
|
expect(component.dismissalNudgeType).toBe(VaultNudgeType.newCardItemStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show anything if spotlight has been dismissed", async () => {
|
||||||
|
component.configType = CipherType.Identity;
|
||||||
|
accountService.activeAccount$ = of({ id: "test-user-id" as UserId } as Account);
|
||||||
|
jest.spyOn(component, "checkHasSpotlightDismissed").mockResolvedValue(false);
|
||||||
|
|
||||||
|
await component.ngOnInit();
|
||||||
|
|
||||||
|
expect(component.showNewItemSpotlight).toBe(false);
|
||||||
|
expect(component.dismissalNudgeType).toBe(VaultNudgeType.newIdentityItemStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set showNewItemSpotlight to false when user dismisses spotlight", async () => {
|
||||||
|
component.showNewItemSpotlight = true;
|
||||||
|
component.dismissalNudgeType = VaultNudgeType.newLoginItemStatus;
|
||||||
|
component.activeUserId = "test-user-id" as UserId;
|
||||||
|
|
||||||
|
const dismissSpy = jest.spyOn(vaultNudgesService, "dismissNudge").mockResolvedValue();
|
||||||
|
|
||||||
|
await component.dismissNewItemSpotlight();
|
||||||
|
|
||||||
|
expect(component.showNewItemSpotlight).toBe(false);
|
||||||
|
expect(dismissSpy).toHaveBeenCalledWith(
|
||||||
|
VaultNudgeType.newLoginItemStatus,
|
||||||
|
component.activeUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { NgIf } from "@angular/common";
|
||||||
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherType } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { SpotlightComponent } from "../../../components/spotlight/spotlight.component";
|
||||||
|
import { VaultNudgesService, VaultNudgeType } from "../../../services/vault-nudges.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-new-item-nudge",
|
||||||
|
templateUrl: "./new-item-nudge.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [NgIf, SpotlightComponent],
|
||||||
|
})
|
||||||
|
export class NewItemNudgeComponent implements OnInit {
|
||||||
|
@Input({ required: true }) configType: CipherType | null = null;
|
||||||
|
activeUserId: UserId | null = null;
|
||||||
|
showNewItemSpotlight: boolean = false;
|
||||||
|
nudgeTitle: string = "";
|
||||||
|
nudgeBody: string = "";
|
||||||
|
dismissalNudgeType: VaultNudgeType | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private vaultNudgesService: VaultNudgesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
|
|
||||||
|
switch (this.configType) {
|
||||||
|
case CipherType.Login:
|
||||||
|
this.dismissalNudgeType = VaultNudgeType.newLoginItemStatus;
|
||||||
|
this.nudgeTitle = this.i18nService.t("newLoginNudgeTitle");
|
||||||
|
this.nudgeBody = this.i18nService.t("newLoginNudgeBody");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CipherType.Card:
|
||||||
|
this.dismissalNudgeType = VaultNudgeType.newCardItemStatus;
|
||||||
|
this.nudgeTitle = this.i18nService.t("newCardNudgeTitle");
|
||||||
|
this.nudgeBody = this.i18nService.t("newCardNudgeBody");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CipherType.Identity:
|
||||||
|
this.dismissalNudgeType = VaultNudgeType.newIdentityItemStatus;
|
||||||
|
this.nudgeTitle = this.i18nService.t("newIdentityNudgeTitle");
|
||||||
|
this.nudgeBody = this.i18nService.t("newIdentityNudgeBody");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
this.dismissalNudgeType = VaultNudgeType.newNoteItemStatus;
|
||||||
|
this.nudgeTitle = this.i18nService.t("newNoteNudgeTitle");
|
||||||
|
this.nudgeBody = this.i18nService.t("newNoteNudgeBody");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CipherType.SshKey:
|
||||||
|
this.dismissalNudgeType = VaultNudgeType.newSshItemStatus;
|
||||||
|
this.nudgeTitle = this.i18nService.t("newSshNudgeTitle");
|
||||||
|
this.nudgeBody = this.i18nService.t("newSshNudgeBody");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported cipher type");
|
||||||
|
}
|
||||||
|
this.showNewItemSpotlight = await this.checkHasSpotlightDismissed(
|
||||||
|
this.dismissalNudgeType as VaultNudgeType,
|
||||||
|
this.activeUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dismissNewItemSpotlight() {
|
||||||
|
if (this.dismissalNudgeType && this.activeUserId) {
|
||||||
|
await this.vaultNudgesService.dismissNudge(
|
||||||
|
this.dismissalNudgeType,
|
||||||
|
this.activeUserId as UserId,
|
||||||
|
);
|
||||||
|
this.showNewItemSpotlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkHasSpotlightDismissed(nudgeType: VaultNudgeType, userId: UserId): Promise<boolean> {
|
||||||
|
return !(await firstValueFrom(this.vaultNudgesService.showNudge$(nudgeType, userId)))
|
||||||
|
.hasSpotlightDismissed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,6 @@ export class HasNudgeService extends DefaultSingleNudgeService {
|
|||||||
|
|
||||||
private nudgeTypes: VaultNudgeType[] = [
|
private nudgeTypes: VaultNudgeType[] = [
|
||||||
VaultNudgeType.EmptyVaultNudge,
|
VaultNudgeType.EmptyVaultNudge,
|
||||||
VaultNudgeType.HasVaultItems,
|
|
||||||
VaultNudgeType.IntroCarouselDismissal,
|
|
||||||
// add additional nudge types here as needed
|
// add additional nudge types here as needed
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./has-items-nudge.service";
|
export * from "./has-items-nudge.service";
|
||||||
export * from "./empty-vault-nudge.service";
|
export * from "./empty-vault-nudge.service";
|
||||||
export * from "./has-nudge.service";
|
export * from "./has-nudge.service";
|
||||||
|
export * from "./new-item-nudge.service";
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { combineLatest, Observable, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
|
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||||
|
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Nudge Service Checking Nudge Status For Vault New Item Types
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class NewItemNudgeService extends DefaultSingleNudgeService {
|
||||||
|
cipherService = inject(CipherService);
|
||||||
|
|
||||||
|
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||||
|
return combineLatest([
|
||||||
|
this.getNudgeStatus$(nudgeType, userId),
|
||||||
|
this.cipherService.cipherViews$(userId),
|
||||||
|
]).pipe(
|
||||||
|
switchMap(async ([nudgeStatus, ciphers]) => {
|
||||||
|
if (nudgeStatus.hasSpotlightDismissed) {
|
||||||
|
return nudgeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentType: CipherType;
|
||||||
|
|
||||||
|
switch (nudgeType) {
|
||||||
|
case VaultNudgeType.newLoginItemStatus:
|
||||||
|
currentType = CipherType.Login;
|
||||||
|
break;
|
||||||
|
case VaultNudgeType.newCardItemStatus:
|
||||||
|
currentType = CipherType.Card;
|
||||||
|
break;
|
||||||
|
case VaultNudgeType.newIdentityItemStatus:
|
||||||
|
currentType = CipherType.Identity;
|
||||||
|
break;
|
||||||
|
case VaultNudgeType.newNoteItemStatus:
|
||||||
|
currentType = CipherType.SecureNote;
|
||||||
|
break;
|
||||||
|
case VaultNudgeType.newSshItemStatus:
|
||||||
|
currentType = CipherType.SshKey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ciphersBoolean = ciphers.some((cipher) => cipher.type === currentType);
|
||||||
|
|
||||||
|
if (ciphersBoolean) {
|
||||||
|
const dismissedStatus = {
|
||||||
|
hasSpotlightDismissed: true,
|
||||||
|
hasBadgeDismissed: true,
|
||||||
|
};
|
||||||
|
await this.setNudgeStatus(nudgeType, dismissedStatus, userId);
|
||||||
|
return dismissedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nudgeStatus;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { firstValueFrom, of } from "rxjs";
|
|||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
|
||||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec";
|
import { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec";
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ describe("Vault Nudges Service", () => {
|
|||||||
provide: EmptyVaultNudgeService,
|
provide: EmptyVaultNudgeService,
|
||||||
useValue: mock<EmptyVaultNudgeService>(),
|
useValue: mock<EmptyVaultNudgeService>(),
|
||||||
},
|
},
|
||||||
|
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
import { UserKeyDefinition, VAULT_NUDGES_DISK } from "@bitwarden/common/platform/state";
|
import { UserKeyDefinition, VAULT_NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { HasItemsNudgeService, EmptyVaultNudgeService } from "./custom-nudges-services";
|
import {
|
||||||
|
HasItemsNudgeService,
|
||||||
|
EmptyVaultNudgeService,
|
||||||
|
NewItemNudgeService,
|
||||||
|
} from "./custom-nudges-services";
|
||||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||||
|
|
||||||
export type NudgeStatus = {
|
export type NudgeStatus = {
|
||||||
@@ -23,7 +27,11 @@ export enum VaultNudgeType {
|
|||||||
*/
|
*/
|
||||||
EmptyVaultNudge = "empty-vault-nudge",
|
EmptyVaultNudge = "empty-vault-nudge",
|
||||||
HasVaultItems = "has-vault-items",
|
HasVaultItems = "has-vault-items",
|
||||||
IntroCarouselDismissal = "intro-carousel-dismissal",
|
newLoginItemStatus = "new-login-item-status",
|
||||||
|
newCardItemStatus = "new-card-item-status",
|
||||||
|
newIdentityItemStatus = "new-identity-item-status",
|
||||||
|
newNoteItemStatus = "new-note-item-status",
|
||||||
|
newSshItemStatus = "new-ssh-item-status",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||||
@@ -37,6 +45,8 @@ export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
|||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class VaultNudgesService {
|
export class VaultNudgesService {
|
||||||
|
private newItemNudgeService = inject(NewItemNudgeService);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom nudge services to use for specific nudge types
|
* Custom nudge services to use for specific nudge types
|
||||||
* 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
|
||||||
@@ -45,6 +55,11 @@ export class VaultNudgesService {
|
|||||||
private customNudgeServices: any = {
|
private customNudgeServices: any = {
|
||||||
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||||
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||||
|
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
|
||||||
|
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,
|
||||||
|
[VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService,
|
||||||
|
[VaultNudgeType.newNoteItemStatus]: this.newItemNudgeService,
|
||||||
|
[VaultNudgeType.newSshItemStatus]: this.newItemNudgeService,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user