mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-18800] vault onboarding nudges and badge (#14278)
* added empty vault nudge service and has items vault nudge service with spotlight and settings badge to vault v2 in browser * Refactor Vault Nudge Service for clarity between spotlight and badge dismissals
This commit is contained in:
@@ -5201,6 +5201,12 @@
|
|||||||
"changeAtRiskPassword": {
|
"changeAtRiskPassword": {
|
||||||
"message": "Change at-risk password"
|
"message": "Change at-risk password"
|
||||||
},
|
},
|
||||||
|
"settingsVaultOptions": {
|
||||||
|
"message": "Vault options"
|
||||||
|
},
|
||||||
|
"emptyVaultDescription": {
|
||||||
|
"message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here."
|
||||||
|
},
|
||||||
"introCarouselLabel": {
|
"introCarouselLabel": {
|
||||||
"message": "Welcome to Bitwarden"
|
"message": "Welcome to Bitwarden"
|
||||||
},
|
},
|
||||||
@@ -5227,5 +5233,20 @@
|
|||||||
},
|
},
|
||||||
"secureDevicesBody": {
|
"secureDevicesBody": {
|
||||||
"message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps."
|
"message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps."
|
||||||
|
},
|
||||||
|
"emptyVaultNudgeTitle": {
|
||||||
|
"message": "Import existing passwords"
|
||||||
|
},
|
||||||
|
"emptyVaultNudgeBody": {
|
||||||
|
"message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them."
|
||||||
|
},
|
||||||
|
"emptyVaultNudgeButton": {
|
||||||
|
"message": "Import now"
|
||||||
|
},
|
||||||
|
"hasItemsVaultNudgeTitle": {
|
||||||
|
"message": "Welcome to your vault!"
|
||||||
|
},
|
||||||
|
"hasItemsVaultNudgeBody": {
|
||||||
|
"message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,9 +18,9 @@ export class TabsV2Component {
|
|||||||
|
|
||||||
protected navButtons$ = combineLatest([
|
protected navButtons$ = combineLatest([
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||||
this.hasNudgeService.shouldShowNudge$(),
|
this.hasNudgeService.nudgeStatus$(),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([onboardingFeatureEnabled, showNudge]) => {
|
map(([onboardingFeatureEnabled, nudgeStatus]) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: "vault",
|
label: "vault",
|
||||||
@@ -45,7 +45,7 @@ export class TabsV2Component {
|
|||||||
page: "/tabs/settings",
|
page: "/tabs/settings",
|
||||||
iconKey: "cog",
|
iconKey: "cog",
|
||||||
iconKeyActive: "cog-f",
|
iconKeyActive: "cog-f",
|
||||||
showBerry: onboardingFeatureEnabled && showNudge,
|
showBerry: onboardingFeatureEnabled && !nudgeStatus.hasSpotlightDismissed,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -29,9 +29,26 @@
|
|||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
<bit-item>
|
<bit-item>
|
||||||
<a bit-item-content routerLink="/vault-settings">
|
<a
|
||||||
|
bit-item-content
|
||||||
|
routerLink="/vault-settings"
|
||||||
|
(click)="dismissBadge(VaultNudgeType.EmptyVaultNudge)"
|
||||||
|
>
|
||||||
<i slot="start" class="bwi bwi-vault" aria-hidden="true"></i>
|
<i slot="start" class="bwi bwi-vault" aria-hidden="true"></i>
|
||||||
{{ "vault" | i18n }}
|
<div class="tw-flex tw-items-center tw-justify-center">
|
||||||
|
<p class="tw-pr-2">{{ "settingsVaultOptions" | i18n }}</p>
|
||||||
|
<!--
|
||||||
|
Currently can be only 1 item for notification.
|
||||||
|
Will make this dynamic when more nudges are added
|
||||||
|
-->
|
||||||
|
<span
|
||||||
|
*ngIf="!(showVaultBadge$ | async)?.hasBadgeDismissed"
|
||||||
|
bitBadge
|
||||||
|
variant="notification"
|
||||||
|
>1</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ItemModule } from "@bitwarden/components";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { BadgeComponent, ItemModule } from "@bitwarden/components";
|
||||||
|
import { NudgeStatus, VaultNudgesService, VaultNudgeType } 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";
|
||||||
@@ -22,6 +27,29 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
PopOutComponent,
|
PopOutComponent,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
CurrentAccountComponent,
|
CurrentAccountComponent,
|
||||||
|
BadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsV2Component {}
|
export class SettingsV2Component implements OnInit {
|
||||||
|
VaultNudgeType = VaultNudgeType;
|
||||||
|
showVaultBadge$: Observable<NudgeStatus> = new Observable();
|
||||||
|
activeUserId: UserId | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly vaultNudgesService: VaultNudgesService,
|
||||||
|
private readonly accountService: AccountService,
|
||||||
|
) {}
|
||||||
|
async ngOnInit() {
|
||||||
|
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
|
this.showVaultBadge$ = this.vaultNudgesService.showNudge$(
|
||||||
|
VaultNudgeType.EmptyVaultNudge,
|
||||||
|
this.activeUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dismissBadge(type: VaultNudgeType) {
|
||||||
|
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) {
|
||||||
|
await this.vaultNudgesService.dismissNudge(type, this.activeUserId as UserId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
>
|
>
|
||||||
<bit-no-items [icon]="vaultIcon">
|
<bit-no-items [icon]="vaultIcon">
|
||||||
<ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container>
|
<ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container>
|
||||||
<ng-container slot="description">{{ "autofillSuggestionsTip" | i18n }}</ng-container>
|
<ng-container slot="description">
|
||||||
|
<p bitTypography="body2" class="tw-mx-6">{{ "emptyVaultDescription" | i18n }}</p>
|
||||||
|
</ng-container>
|
||||||
<app-new-item-dropdown
|
<app-new-item-dropdown
|
||||||
slot="button"
|
slot="button"
|
||||||
[initialValues]="newItemItemValues$ | async"
|
[initialValues]="newItemItemValues$ | async"
|
||||||
@@ -28,11 +30,31 @@
|
|||||||
></blocked-injection-banner>
|
></blocked-injection-banner>
|
||||||
|
|
||||||
<!-- Show search & filters outside of the scroll area of the page -->
|
<!-- Show search & filters outside of the scroll area of the page -->
|
||||||
<ng-container slot="above-scroll-area" *ngIf="vaultState !== VaultStateEnum.Empty">
|
<ng-container slot="above-scroll-area">
|
||||||
<vault-at-risk-password-callout
|
<ng-container *ngIf="vaultState === VaultStateEnum.Empty && showEmptyVaultSpotlight$ | async">
|
||||||
*appIfFeature="FeatureFlag.SecurityTasks"
|
<bit-spotlight
|
||||||
></vault-at-risk-password-callout>
|
[title]="'emptyVaultNudgeTitle' | i18n"
|
||||||
<app-vault-header-v2></app-vault-header-v2>
|
[subtitle]="'emptyVaultNudgeBody' | i18n"
|
||||||
|
[buttonText]="'emptyVaultNudgeButton' | i18n"
|
||||||
|
(onButtonClick)="navigateToImport()"
|
||||||
|
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.EmptyVaultNudge)"
|
||||||
|
>
|
||||||
|
</bit-spotlight>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
|
||||||
|
<div class="tw-mb-4" *ngIf="showHasItemsVaultSpotlight$ | async">
|
||||||
|
<bit-spotlight
|
||||||
|
[title]="'hasItemsVaultNudgeTitle' | i18n"
|
||||||
|
[subtitle]="'hasItemsVaultNudgeBody' | i18n"
|
||||||
|
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.HasVaultItems)"
|
||||||
|
>
|
||||||
|
</bit-spotlight>
|
||||||
|
</div>
|
||||||
|
<vault-at-risk-password-callout
|
||||||
|
*appIfFeature="FeatureFlag.SecurityTasks"
|
||||||
|
></vault-at-risk-password-callout>
|
||||||
|
<app-vault-header-v2></app-vault-header-v2>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
|
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
|
||||||
|
|||||||
@@ -2,30 +2,38 @@ import { CdkVirtualScrollableElement, ScrollingModule } from "@angular/cdk/scrol
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
filter,
|
filter,
|
||||||
map,
|
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
startWith,
|
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
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 { 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 { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
|
import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
|
||||||
import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault";
|
import {
|
||||||
|
DecryptionFailureDialogComponent,
|
||||||
|
SpotlightComponent,
|
||||||
|
VaultIcons,
|
||||||
|
VaultNudgesService,
|
||||||
|
VaultNudgeType,
|
||||||
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
|
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
|
||||||
|
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||||
@@ -74,14 +82,29 @@ enum VaultState {
|
|||||||
VaultHeaderV2Component,
|
VaultHeaderV2Component,
|
||||||
AtRiskPasswordCalloutComponent,
|
AtRiskPasswordCalloutComponent,
|
||||||
NewSettingsCalloutComponent,
|
NewSettingsCalloutComponent,
|
||||||
|
SpotlightComponent,
|
||||||
|
RouterModule,
|
||||||
],
|
],
|
||||||
providers: [VaultPageService],
|
providers: [VaultPageService],
|
||||||
})
|
})
|
||||||
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
|
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
|
||||||
|
|
||||||
|
VaultNudgeType = VaultNudgeType;
|
||||||
cipherType = CipherType;
|
cipherType = CipherType;
|
||||||
|
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
showEmptyVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
|
||||||
|
switchMap((userId) =>
|
||||||
|
this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, userId),
|
||||||
|
),
|
||||||
|
map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed),
|
||||||
|
);
|
||||||
|
showHasItemsVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
|
||||||
|
switchMap((userId) => this.vaultNudgesService.showNudge$(VaultNudgeType.HasVaultItems, userId)),
|
||||||
|
map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed),
|
||||||
|
);
|
||||||
|
|
||||||
|
activeUserId: UserId | null = null;
|
||||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||||
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||||
protected allFilters$ = this.vaultPopupListFiltersService.allFilters$;
|
protected allFilters$ = this.vaultPopupListFiltersService.allFilters$;
|
||||||
@@ -131,7 +154,8 @@ 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 configService: ConfigService,
|
private vaultNudgesService: VaultNudgesService,
|
||||||
|
private router: Router,
|
||||||
) {
|
) {
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.vaultPopupItemsService.emptyVault$,
|
this.vaultPopupItemsService.emptyVault$,
|
||||||
@@ -169,16 +193,12 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const hasVaultNudgeFlag = await this.configService.getFeatureFlag(
|
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
|
||||||
);
|
await this.introCarouselService.setIntroCarouselDismissed();
|
||||||
if (hasVaultNudgeFlag) {
|
|
||||||
await this.introCarouselService.setIntroCarouselDismissed();
|
|
||||||
}
|
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
|
||||||
|
|
||||||
this.cipherService
|
this.cipherService
|
||||||
.failedToDecryptCiphers$(activeUserId)
|
.failedToDecryptCiphers$(this.activeUserId)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((ciphers) => (ciphers ? ciphers.filter((c) => !c.isDeleted) : [])),
|
map((ciphers) => (ciphers ? ciphers.filter((c) => !c.isDeleted) : [])),
|
||||||
filter((ciphers) => ciphers.length > 0),
|
filter((ciphers) => ciphers.length > 0),
|
||||||
@@ -196,5 +216,16 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.vaultScrollPositionService.stop();
|
this.vaultScrollPositionService.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async navigateToImport() {
|
||||||
|
await this.router.navigate(["/import"]);
|
||||||
|
if (await BrowserApi.isPopupOpen()) {
|
||||||
|
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async dismissVaultNudgeSpotlight(type: VaultNudgeType) {
|
||||||
|
await this.vaultNudgesService.dismissNudge(type, this.activeUserId as UserId);
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly FeatureFlag = FeatureFlag;
|
protected readonly FeatureFlag = FeatureFlag;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { map, Observable } from "rxjs";
|
import { firstValueFrom, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import {
|
import {
|
||||||
GlobalState,
|
GlobalState,
|
||||||
KeyDefinition,
|
KeyDefinition,
|
||||||
@@ -26,9 +28,17 @@ export class IntroCarouselService {
|
|||||||
map((x) => x ?? false),
|
map((x) => x ?? false),
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {}
|
constructor(
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async setIntroCarouselDismissed(): Promise<void> {
|
async setIntroCarouselDismissed(): Promise<void> {
|
||||||
await this.introCarouselState.update(() => true);
|
const hasVaultNudgeFlag = await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||||
|
);
|
||||||
|
if (hasVaultNudgeFlag) {
|
||||||
|
await this.introCarouselState.update(() => true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,3 +32,5 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
|||||||
|
|
||||||
export * from "./abstractions/change-login-password.service";
|
export * from "./abstractions/change-login-password.service";
|
||||||
export * from "./services/default-change-login-password.service";
|
export * from "./services/default-change-login-password.service";
|
||||||
|
|
||||||
|
export { SpotlightComponent } from "./components/spotlight/spotlight.component";
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
|
||||||
|
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||||
|
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Nudge Service Checking Nudge Status For Empty Vault
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||||
|
cipherService = inject(CipherService);
|
||||||
|
organizationService = inject(OrganizationService);
|
||||||
|
collectionService = inject(CollectionService);
|
||||||
|
|
||||||
|
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||||
|
return combineLatest([
|
||||||
|
this.getNudgeStatus$(nudgeType, userId),
|
||||||
|
this.cipherService.cipherViews$(userId),
|
||||||
|
this.organizationService.organizations$(userId),
|
||||||
|
this.collectionService.decryptedCollections$,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||||
|
const emptyVault = ciphers == null || ciphers.length === 0;
|
||||||
|
if (orgs == null || orgs.length === 0) {
|
||||||
|
return nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed
|
||||||
|
? of(nudgeStatus)
|
||||||
|
: of({
|
||||||
|
hasSpotlightDismissed: emptyVault,
|
||||||
|
hasBadgeDismissed: emptyVault,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const orgIds = new Set(orgs.map((org) => org.id));
|
||||||
|
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||||
|
const hasManageCollections = collections.some(
|
||||||
|
(c) => c.manage && orgIds.has(c.organizationId),
|
||||||
|
);
|
||||||
|
// Do not show nudge when
|
||||||
|
// user has previously dismissed nudge
|
||||||
|
// OR
|
||||||
|
// user belongs to an organization and cannot create collections || manage collections
|
||||||
|
if (
|
||||||
|
nudgeStatus.hasBadgeDismissed ||
|
||||||
|
nudgeStatus.hasSpotlightDismissed ||
|
||||||
|
hasManageCollections ||
|
||||||
|
canCreateCollections
|
||||||
|
) {
|
||||||
|
return of(nudgeStatus);
|
||||||
|
}
|
||||||
|
return of({
|
||||||
|
hasSpotlightDismissed: emptyVault,
|
||||||
|
hasBadgeDismissed: emptyVault,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,49 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from "@angular/core";
|
||||||
import { map, Observable, of, switchMap } from "rxjs";
|
import { combineLatest, Observable, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
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 { VaultNudgeType } from "../vault-nudges.service";
|
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Nudge Service to use for the Onboarding Nudges in the Vault
|
* Custom Nudge Service Checking Nudge Status For Welcome Nudge With Populated Vault
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class HasItemsNudgeService extends DefaultSingleNudgeService {
|
export class HasItemsNudgeService extends DefaultSingleNudgeService {
|
||||||
cipherService = inject(CipherService);
|
cipherService = inject(CipherService);
|
||||||
|
vaultProfileService = inject(VaultProfileService);
|
||||||
|
logService = inject(LogService);
|
||||||
|
|
||||||
shouldShowNudge$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean> {
|
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||||
return this.isDismissed$(nudgeType, userId).pipe(
|
return combineLatest([
|
||||||
switchMap((dismissed) =>
|
this.cipherService.cipherViews$(userId),
|
||||||
dismissed
|
this.getNudgeStatus$(nudgeType, userId),
|
||||||
? of(false)
|
]).pipe(
|
||||||
: this.cipherService
|
switchMap(async ([ciphers, nudgeStatus]) => {
|
||||||
.cipherViews$(userId)
|
try {
|
||||||
.pipe(map((ciphers) => ciphers == null || ciphers.length === 0)),
|
const creationDate = await this.vaultProfileService.getProfileCreationDate(userId);
|
||||||
),
|
const thirtyDays = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
const isRecentAcct = creationDate >= thirtyDays;
|
||||||
|
|
||||||
|
if (!isRecentAcct || nudgeStatus.hasSpotlightDismissed) {
|
||||||
|
return nudgeStatus;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
hasBadgeDismissed: ciphers == null || ciphers.length === 0,
|
||||||
|
hasSpotlightDismissed: ciphers == null || ciphers.length === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logService.error("Failed to fetch profile creation date: ", error);
|
||||||
|
return nudgeStatus;
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
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 { VaultNudgeType } from "../vault-nudges.service";
|
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Nudge Service used for showing if the user has any existing nudge in the Vault.
|
* Custom Nudge Service used for showing if the user has any existing nudge in the Vault.
|
||||||
@@ -17,6 +17,7 @@ export class HasNudgeService extends DefaultSingleNudgeService {
|
|||||||
private accountService = inject(AccountService);
|
private accountService = inject(AccountService);
|
||||||
|
|
||||||
private nudgeTypes: VaultNudgeType[] = [
|
private nudgeTypes: VaultNudgeType[] = [
|
||||||
|
VaultNudgeType.EmptyVaultNudge,
|
||||||
VaultNudgeType.HasVaultItems,
|
VaultNudgeType.HasVaultItems,
|
||||||
VaultNudgeType.IntroCarouselDismissal,
|
VaultNudgeType.IntroCarouselDismissal,
|
||||||
// add additional nudge types here as needed
|
// add additional nudge types here as needed
|
||||||
@@ -25,20 +26,25 @@ export class HasNudgeService extends DefaultSingleNudgeService {
|
|||||||
/**
|
/**
|
||||||
* Returns an observable that emits true if any of the provided nudge types are present
|
* Returns an observable that emits true if any of the provided nudge types are present
|
||||||
*/
|
*/
|
||||||
shouldShowNudge$(): Observable<boolean> {
|
nudgeStatus$(): Observable<NudgeStatus> {
|
||||||
return this.accountService.activeAccount$.pipe(
|
return this.accountService.activeAccount$.pipe(
|
||||||
switchMap((activeAccount) => {
|
switchMap((activeAccount) => {
|
||||||
const userId: UserId | undefined = activeAccount?.id;
|
const userId: UserId | undefined = activeAccount?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return of(false);
|
return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const nudgeObservables: Observable<boolean>[] = this.nudgeTypes.map((nudge) =>
|
const nudgeObservables: Observable<NudgeStatus>[] = this.nudgeTypes.map((nudge) =>
|
||||||
super.shouldShowNudge$(nudge, userId),
|
super.nudgeStatus$(nudge, userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return combineLatest(nudgeObservables).pipe(
|
return combineLatest(nudgeObservables).pipe(
|
||||||
map((nudgeStates) => nudgeStates.some((state) => state)),
|
map((nudgeStates) => {
|
||||||
|
return {
|
||||||
|
hasBadgeDismissed: true,
|
||||||
|
hasSpotlightDismissed: nudgeStates.some((state) => state.hasSpotlightDismissed),
|
||||||
|
};
|
||||||
|
}),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./has-items-nudge.service";
|
export * from "./has-items-nudge.service";
|
||||||
|
export * from "./empty-vault-nudge.service";
|
||||||
export * from "./has-nudge.service";
|
export * from "./has-nudge.service";
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ 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 { VAULT_NUDGE_DISMISSED_DISK_KEY, VaultNudgeType } from "./vault-nudges.service";
|
import {
|
||||||
|
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 {
|
||||||
shouldShowNudge$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean>;
|
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus>;
|
||||||
|
|
||||||
setNudgeStatus(nudgeType: VaultNudgeType, dismissed: boolean, userId: UserId): Promise<void>;
|
setNudgeStatus(nudgeType: VaultNudgeType, newStatus: NudgeStatus, userId: UserId): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,28 +28,29 @@ export interface SingleNudgeService {
|
|||||||
export class DefaultSingleNudgeService implements SingleNudgeService {
|
export class DefaultSingleNudgeService implements SingleNudgeService {
|
||||||
stateProvider = inject(StateProvider);
|
stateProvider = inject(StateProvider);
|
||||||
|
|
||||||
protected isDismissed$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean> {
|
protected getNudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||||
return this.stateProvider
|
return this.stateProvider
|
||||||
.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY)
|
.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY)
|
||||||
.state$.pipe(map((nudges) => nudges?.includes(nudgeType) ?? false));
|
.state$.pipe(
|
||||||
|
map(
|
||||||
|
(nudges) =>
|
||||||
|
nudges?.[nudgeType] ?? { hasBadgeDismissed: false, hasSpotlightDismissed: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldShowNudge$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean> {
|
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||||
return this.isDismissed$(nudgeType, userId).pipe(map((dismissed) => !dismissed));
|
return this.getNudgeStatus$(nudgeType, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setNudgeStatus(
|
async setNudgeStatus(
|
||||||
nudgeType: VaultNudgeType,
|
nudgeType: VaultNudgeType,
|
||||||
dismissed: boolean,
|
status: NudgeStatus,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.stateProvider.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
|
await this.stateProvider.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
|
||||||
nudges ??= [];
|
nudges ??= {};
|
||||||
if (dismissed) {
|
nudges[nudgeType] = status;
|
||||||
nudges.push(nudgeType);
|
|
||||||
} else {
|
|
||||||
nudges = nudges.filter((n) => n !== nudgeType);
|
|
||||||
}
|
|
||||||
return nudges;
|
return nudges;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ 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 { 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 { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec";
|
import { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec";
|
||||||
|
|
||||||
import { HasItemsNudgeService } from "./custom-nudges-services/has-items-nudge.service";
|
import { HasItemsNudgeService, EmptyVaultNudgeService } 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 { VaultNudgesService, VaultNudgeType } from "./vault-nudges.service";
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ describe("Vault Nudges Service", () => {
|
|||||||
let fakeStateProvider: FakeStateProvider;
|
let fakeStateProvider: FakeStateProvider;
|
||||||
|
|
||||||
let testBed: TestBed;
|
let testBed: TestBed;
|
||||||
|
const mockConfigService = {
|
||||||
|
getFeatureFlag$: jest.fn().mockReturnValue(of(true)),
|
||||||
|
getFeatureFlag: jest.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||||
@@ -32,50 +37,55 @@ describe("Vault Nudges Service", () => {
|
|||||||
provide: StateProvider,
|
provide: StateProvider,
|
||||||
useValue: fakeStateProvider,
|
useValue: fakeStateProvider,
|
||||||
},
|
},
|
||||||
|
{ provide: ConfigService, useValue: mockConfigService },
|
||||||
{
|
{
|
||||||
provide: HasItemsNudgeService,
|
provide: HasItemsNudgeService,
|
||||||
useValue: mock<HasItemsNudgeService>(),
|
useValue: mock<HasItemsNudgeService>(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: EmptyVaultNudgeService,
|
||||||
|
useValue: mock<EmptyVaultNudgeService>(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DefaultSingleNudgeService", () => {
|
describe("DefaultSingleNudgeService", () => {
|
||||||
it("should return shouldShowNudge === false when IntroCaourselDismissal dismissed is true", async () => {
|
it("should return hasSpotlightDismissed === true when EmptyVaultNudge dismissed is true", async () => {
|
||||||
const service = testBed.inject(DefaultSingleNudgeService);
|
const service = testBed.inject(DefaultSingleNudgeService);
|
||||||
|
|
||||||
await service.setNudgeStatus(
|
await service.setNudgeStatus(
|
||||||
VaultNudgeType.IntroCarouselDismissal,
|
VaultNudgeType.EmptyVaultNudge,
|
||||||
true,
|
{ hasBadgeDismissed: true, hasSpotlightDismissed: true },
|
||||||
"user-id" as UserId,
|
"user-id" as UserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
service.shouldShowNudge$(VaultNudgeType.IntroCarouselDismissal, "user-id" as UserId),
|
service.nudgeStatus$(VaultNudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||||
);
|
);
|
||||||
expect(result).toBe(false);
|
expect(result).toEqual({ hasBadgeDismissed: true, hasSpotlightDismissed: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return shouldShowNudge === true when IntroCaourselDismissal dismissed is false", async () => {
|
it("should return hasSpotlightDismissed === true when EmptyVaultNudge dismissed is false", async () => {
|
||||||
const service = testBed.inject(DefaultSingleNudgeService);
|
const service = testBed.inject(DefaultSingleNudgeService);
|
||||||
|
|
||||||
await service.setNudgeStatus(
|
await service.setNudgeStatus(
|
||||||
VaultNudgeType.IntroCarouselDismissal,
|
VaultNudgeType.EmptyVaultNudge,
|
||||||
false,
|
{ hasBadgeDismissed: false, hasSpotlightDismissed: false },
|
||||||
"user-id" as UserId,
|
"user-id" as UserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
service.shouldShowNudge$(VaultNudgeType.IntroCarouselDismissal, "user-id" as UserId),
|
service.nudgeStatus$(VaultNudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||||
);
|
);
|
||||||
expect(result).toBe(true);
|
expect(result).toEqual({ hasBadgeDismissed: false, hasSpotlightDismissed: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("VaultNudgesService", () => {
|
describe("VaultNudgesService", () => {
|
||||||
it("should return true, the proper value from the custom nudge service shouldShowNudge$", async () => {
|
it("should return true, the proper value from the custom nudge service nudgeStatus$", async () => {
|
||||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||||
useValue: { shouldShowNudge$: () => of(true) },
|
useValue: { nudgeStatus$: () => of(true) },
|
||||||
});
|
});
|
||||||
const service = testBed.inject(VaultNudgesService);
|
const service = testBed.inject(VaultNudgesService);
|
||||||
|
|
||||||
@@ -86,9 +96,9 @@ describe("Vault Nudges Service", () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false, the proper value for the custom nudge service shouldShowNudge$", async () => {
|
it("should return false, the proper value for the custom nudge service nudgeStatus$", async () => {
|
||||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||||
useValue: { shouldShowNudge$: () => of(false) },
|
useValue: { nudgeStatus$: () => of(false) },
|
||||||
});
|
});
|
||||||
const service = testBed.inject(VaultNudgesService);
|
const service = testBed.inject(VaultNudgesService);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { of, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
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 } from "./custom-nudges-services/has-items-nudge.service";
|
import { HasItemsNudgeService, EmptyVaultNudgeService } from "./custom-nudges-services";
|
||||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||||
|
|
||||||
|
export type NudgeStatus = {
|
||||||
|
hasBadgeDismissed: boolean;
|
||||||
|
hasSpotlightDismissed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum to list the various nudge types, to be used by components/badges to show/hide the nudge
|
* Enum to list the various nudge types, to be used by components/badges to show/hide the nudge
|
||||||
*/
|
*/
|
||||||
@@ -13,18 +21,17 @@ export enum VaultNudgeType {
|
|||||||
/** 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
|
||||||
*/
|
*/
|
||||||
|
EmptyVaultNudge = "empty-vault-nudge",
|
||||||
HasVaultItems = "has-vault-items",
|
HasVaultItems = "has-vault-items",
|
||||||
IntroCarouselDismissal = "intro-carousel-dismissal",
|
IntroCarouselDismissal = "intro-carousel-dismissal",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<VaultNudgeType[]>(
|
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||||
VAULT_NUDGES_DISK,
|
Partial<Record<VaultNudgeType, NudgeStatus>>
|
||||||
"vaultNudgeDismissed",
|
>(VAULT_NUDGES_DISK, "vaultNudgeDismissed", {
|
||||||
{
|
deserializer: (nudge) => nudge,
|
||||||
deserializer: (nudgeDismissed) => nudgeDismissed,
|
clearOn: [], // Do not clear dismissals
|
||||||
clearOn: [], // Do not clear dismissals
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
@@ -37,6 +44,7 @@ export class VaultNudgesService {
|
|||||||
*/
|
*/
|
||||||
private customNudgeServices: any = {
|
private customNudgeServices: any = {
|
||||||
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||||
|
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +53,7 @@ export class VaultNudgesService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
||||||
|
private configService = inject(ConfigService);
|
||||||
|
|
||||||
private getNudgeService(nudge: VaultNudgeType): SingleNudgeService {
|
private getNudgeService(nudge: VaultNudgeType): SingleNudgeService {
|
||||||
return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
|
return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
|
||||||
@@ -56,7 +65,14 @@ export class VaultNudgesService {
|
|||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
showNudge$(nudge: VaultNudgeType, userId: UserId) {
|
showNudge$(nudge: VaultNudgeType, userId: UserId) {
|
||||||
return this.getNudgeService(nudge).shouldShowNudge$(nudge, userId);
|
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||||
|
switchMap((hasVaultNudgeFlag) => {
|
||||||
|
if (!hasVaultNudgeFlag) {
|
||||||
|
return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true } as NudgeStatus);
|
||||||
|
}
|
||||||
|
return this.getNudgeService(nudge).nudgeStatus$(nudge, userId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +80,10 @@ export class VaultNudgesService {
|
|||||||
* @param nudge
|
* @param nudge
|
||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
dismissNudge(nudge: VaultNudgeType, userId: UserId) {
|
async dismissNudge(nudge: VaultNudgeType, userId: UserId, onlyBadge: boolean = false) {
|
||||||
return this.getNudgeService(nudge).setNudgeStatus(nudge, true, userId);
|
const dismissedStatus = onlyBadge
|
||||||
|
? { hasBadgeDismissed: true, hasSpotlightDismissed: false }
|
||||||
|
: { hasBadgeDismissed: true, hasSpotlightDismissed: true };
|
||||||
|
await this.getNudgeService(nudge).setNudgeStatus(nudge, dismissedStatus, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user