1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-21395] Vault Nudges Bugs (#14737)

* updates to empty vault and has items nudges
This commit is contained in:
Jason Ng
2025-05-13 16:49:41 -04:00
committed by GitHub
parent 393926beec
commit d68574fc40
7 changed files with 69 additions and 23 deletions

View File

@@ -5273,8 +5273,14 @@
"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"
"hasItemsVaultNudgeBodyOne": {
"message": "Autofill items for the current page"
},
"hasItemsVaultNudgeBodyTwo": {
"message": "Favorite items for easy access"
},
"hasItemsVaultNudgeBodyThree": {
"message": "Search your vault for something else"
},
"newLoginNudgeTitle": {
"message": "Save time with autofill"

View File

@@ -44,9 +44,13 @@
<div class="tw-mb-4" *ngIf="showHasItemsVaultSpotlight$ | async">
<bit-spotlight
[title]="'hasItemsVaultNudgeTitle' | i18n"
[subtitle]="'hasItemsVaultNudgeBody' | i18n"
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.HasVaultItems)"
>
<ul class="tw-pl-4 tw-text-main" bitTypography="body2">
<li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyTwo" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyThree" | i18n }}</li>
</ul>
</bit-spotlight>
</div>
<vault-at-risk-password-callout

View File

@@ -19,10 +19,17 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
import {
ButtonModule,
DialogService,
Icons,
NoItemsModule,
TypographyModule,
} from "@bitwarden/components";
import {
DecryptionFailureDialogComponent,
SpotlightComponent,
@@ -86,6 +93,7 @@ enum VaultState {
NewSettingsCalloutComponent,
SpotlightComponent,
RouterModule,
TypographyModule,
],
providers: [VaultPageService],
})
@@ -158,6 +166,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private introCarouselService: IntroCarouselService,
private vaultNudgesService: VaultNudgesService,
private router: Router,
private i18nService: I18nService,
) {
combineLatest([
this.vaultPopupItemsService.emptyVault$,

View File

@@ -4,7 +4,13 @@
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
<div>
<h2 bitTypography="h4" class="tw-font-semibold !tw-mb-1">{{ title }}</h2>
<p class="tw-text-main tw-mb-0" bitTypography="body2" [innerHTML]="subtitle"></p>
<p
*ngIf="subtitle"
class="tw-text-main tw-mb-0"
bitTypography="body2"
[innerHTML]="subtitle"
></p>
<ng-content *ngIf="!subtitle"></ng-content>
</div>
<button
type="button"

View File

@@ -14,7 +14,7 @@ export class SpotlightComponent {
// The title of the component
@Input({ required: true }) title: string | null = null;
// The subtitle of the component
@Input({ required: true }) subtitle: string | null = null;
@Input() subtitle?: string | null = null;
// The text to display on the button
@Input() buttonText?: string;
// Wheter the component can be dismissed, if true, the component will not show a close button

View File

@@ -28,7 +28,10 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
this.collectionService.decryptedCollections$,
]).pipe(
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
const filteredCiphers = ciphers?.filter((cipher) => {
return cipher.deletedDate == null;
});
const vaultHasContents = !(filteredCiphers == null || filteredCiphers.length === 0);
if (orgs == null || orgs.length === 0) {
return nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed
? of(nudgeStatus)

View File

@@ -1,5 +1,6 @@
import { inject, Injectable } from "@angular/core";
import { combineLatest, Observable, switchMap } from "rxjs";
import { combineLatest, from, Observable, of, switchMap } from "rxjs";
import { catchError } from "rxjs/operators";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -9,6 +10,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
/**
* Custom Nudge Service Checking Nudge Status For Welcome Nudge With Populated Vault
*/
@@ -21,27 +24,42 @@ export class HasItemsNudgeService extends DefaultSingleNudgeService {
logService = inject(LogService);
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => {
this.logService.error("Error getting profile creation date");
// Default to today to ensure we show the nudge
return of(new Date());
}),
);
return combineLatest([
this.cipherService.cipherViews$(userId),
this.getNudgeStatus$(nudgeType, userId),
profileDate$,
of(Date.now() - THIRTY_DAYS_MS),
]).pipe(
switchMap(async ([ciphers, nudgeStatus]) => {
try {
const creationDate = await this.vaultProfileService.getProfileCreationDate(userId);
const thirtyDays = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000);
const isRecentAcct = creationDate >= thirtyDays;
switchMap(async ([ciphers, nudgeStatus, profileDate, profileCutoff]) => {
const profileOlderThanCutoff = profileDate.getTime() < profileCutoff;
const filteredCiphers = ciphers?.filter((cipher) => {
return cipher.deletedDate == null;
});
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);
if (profileOlderThanCutoff && filteredCiphers.length > 0) {
const dismissedStatus = {
hasSpotlightDismissed: true,
hasBadgeDismissed: true,
};
// 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(VaultNudgeType.EmptyVaultNudge, dismissedStatus, userId);
return dismissedStatus;
} else if (nudgeStatus.hasSpotlightDismissed) {
return nudgeStatus;
} else {
return {
hasBadgeDismissed: filteredCiphers == null || filteredCiphers.length === 0,
hasSpotlightDismissed: filteredCiphers == null || filteredCiphers.length === 0,
};
}
}),
);