From 65f4ff69099dff5a342a3e26238978a125e10cf9 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Wed, 4 Jun 2025 08:07:44 -0500
Subject: [PATCH] [PM-21791] Nudge UI Bug Fixes (#15010)
* remove margin bottom from empty vault nudge
* update page title to vault options
* show badge on import of vault settings
* add margin between no items title and icon
* add mock to test
* add comment for destroying vault settings page
* fix logic for manage/create collection
* account for deleted ciphers when showing the import nudge
* refactor name of vault import nudge
---
.../vault-v2/vault-v2.component.html | 2 +-
.../settings/vault-settings-v2.component.html | 14 +++-
.../settings/vault-settings-v2.component.ts | 26 ++++++-
.../services/custom-nudges-services/index.ts | 1 +
.../vault-settings-import-nudge.service.ts | 74 +++++++++++++++++++
.../src/vault/services/nudges.service.spec.ts | 5 ++
.../src/vault/services/nudges.service.ts | 3 +
.../src/no-items/no-items.component.html | 2 +-
8 files changed, 120 insertions(+), 7 deletions(-)
create mode 100644 libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
index da7b139359..ddd26b7742 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
@@ -46,7 +46,7 @@
[title]="'hasItemsVaultNudgeTitle' | i18n"
(onDismiss)="dismissVaultNudgeSpotlight(NudgeType.HasVaultItems)"
>
-
+
- {{ "hasItemsVaultNudgeBodyOne" | i18n }}
- {{ "hasItemsVaultNudgeBodyTwo" | i18n }}
- {{ "hasItemsVaultNudgeBodyThree" | i18n }}
diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html
index 03dd1182fb..4e16f58d7f 100644
--- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html
+++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html
@@ -1,5 +1,5 @@
-
+
@@ -14,7 +14,17 @@
diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts
index 9efdc56899..6f7940a282 100644
--- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts
+++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts
@@ -1,11 +1,15 @@
import { CommonModule } from "@angular/common";
-import { Component, OnInit } from "@angular/core";
+import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
+import { firstValueFrom, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
+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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
-import { ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
+import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
@@ -23,22 +27,38 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
PopupHeaderComponent,
PopOutComponent,
ItemModule,
+ BadgeComponent,
],
})
-export class VaultSettingsV2Component implements OnInit {
+export class VaultSettingsV2Component implements OnInit, OnDestroy {
lastSync = "--";
+ protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) =>
+ this.nudgeService.showNudgeBadge$(NudgeType.VaultSettingsImportNudge, userId),
+ ),
+ );
+
constructor(
private router: Router,
private syncService: SyncService,
private toastService: ToastService,
private i18nService: I18nService,
+ private nudgeService: NudgesService,
+ private accountService: AccountService,
) {}
async ngOnInit() {
await this.setLastSync();
}
+ async ngOnDestroy(): Promise {
+ // When a user navigates away from the page, dismiss the empty vault import nudge
+ const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.nudgeService.dismissNudge(NudgeType.VaultSettingsImportNudge, userId);
+ }
+
async import() {
await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) {
diff --git a/libs/angular/src/vault/services/custom-nudges-services/index.ts b/libs/angular/src/vault/services/custom-nudges-services/index.ts
index e94b8bf71e..10b6b45aa1 100644
--- a/libs/angular/src/vault/services/custom-nudges-services/index.ts
+++ b/libs/angular/src/vault/services/custom-nudges-services/index.ts
@@ -3,4 +3,5 @@ export * from "./account-security-nudge.service";
export * from "./has-items-nudge.service";
export * from "./download-bitwarden-nudge.service";
export * from "./empty-vault-nudge.service";
+export * from "./vault-settings-import-nudge.service";
export * from "./new-item-nudge.service";
diff --git a/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts
new file mode 100644
index 0000000000..2d86c76dff
--- /dev/null
+++ b/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts
@@ -0,0 +1,74 @@
+import { inject, Injectable } from "@angular/core";
+import { combineLatest, Observable, of, switchMap } from "rxjs";
+
+// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
+// eslint-disable-next-line no-restricted-imports
+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, NudgeType } from "../nudges.service";
+
+/**
+ * Custom Nudge Service for the vault settings import badge.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
+ cipherService = inject(CipherService);
+ organizationService = inject(OrganizationService);
+ collectionService = inject(CollectionService);
+
+ nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable {
+ return combineLatest([
+ this.getNudgeStatus$(nudgeType, userId),
+ this.cipherService.cipherViews$(userId),
+ this.organizationService.organizations$(userId),
+ this.collectionService.decryptedCollections$,
+ ]).pipe(
+ switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
+ const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
+ const { hasBadgeDismissed, hasSpotlightDismissed } = nudgeStatus;
+
+ // When the user has no organizations, return the nudge status directly
+ if ((orgs?.length ?? 0) === 0) {
+ return hasBadgeDismissed || hasSpotlightDismissed
+ ? of(nudgeStatus)
+ : of({
+ hasSpotlightDismissed: vaultHasMoreThanOneItem,
+ hasBadgeDismissed: vaultHasMoreThanOneItem,
+ });
+ }
+
+ 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),
+ );
+
+ // When the user has dismissed the nudge or spotlight, return the nudge status directly
+ if (hasBadgeDismissed || hasSpotlightDismissed) {
+ return of(nudgeStatus);
+ }
+
+ // When the user belongs to an organization and cannot create collections or manage collections,
+ // hide the nudge and spotlight
+ if (!hasManageCollections && !canCreateCollections) {
+ return of({
+ hasSpotlightDismissed: true,
+ hasBadgeDismissed: true,
+ });
+ }
+
+ // Otherwise, return the nudge status based on the vault contents
+ return of({
+ hasSpotlightDismissed: vaultHasMoreThanOneItem,
+ hasBadgeDismissed: vaultHasMoreThanOneItem,
+ });
+ }),
+ );
+ }
+}
diff --git a/libs/angular/src/vault/services/nudges.service.spec.ts b/libs/angular/src/vault/services/nudges.service.spec.ts
index 30e2ada600..db1091c095 100644
--- a/libs/angular/src/vault/services/nudges.service.spec.ts
+++ b/libs/angular/src/vault/services/nudges.service.spec.ts
@@ -20,6 +20,7 @@ import {
HasItemsNudgeService,
EmptyVaultNudgeService,
DownloadBitwardenNudgeService,
+ VaultSettingsImportNudgeService,
} from "./custom-nudges-services";
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
import { NudgesService, NudgeType } from "./nudges.service";
@@ -64,6 +65,10 @@ describe("Vault Nudges Service", () => {
provide: EmptyVaultNudgeService,
useValue: mock(),
},
+ {
+ provide: VaultSettingsImportNudgeService,
+ useValue: mock(),
+ },
{
provide: ApiService,
useValue: mock(),
diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts
index 9ba46a3bb6..25b111d888 100644
--- a/libs/angular/src/vault/services/nudges.service.ts
+++ b/libs/angular/src/vault/services/nudges.service.ts
@@ -13,6 +13,7 @@ import {
DownloadBitwardenNudgeService,
NewItemNudgeService,
AccountSecurityNudgeService,
+ VaultSettingsImportNudgeService,
} from "./custom-nudges-services";
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
@@ -31,6 +32,7 @@ export enum NudgeType {
* Add future nudges here
*/
EmptyVaultNudge = "empty-vault-nudge",
+ VaultSettingsImportNudge = "vault-settings-import-nudge",
HasVaultItems = "has-vault-items",
AutofillNudge = "autofill-nudge",
AccountSecurity = "account-security",
@@ -64,6 +66,7 @@ export class NudgesService {
private customNudgeServices: Partial> = {
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
+ [NudgeType.VaultSettingsImportNudge]: inject(VaultSettingsImportNudgeService),
[NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
[NudgeType.AutofillNudge]: inject(AutofillNudgeService),
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
diff --git a/libs/components/src/no-items/no-items.component.html b/libs/components/src/no-items/no-items.component.html
index bbc78cec3c..ae2416d7a7 100644
--- a/libs/components/src/no-items/no-items.component.html
+++ b/libs/components/src/no-items/no-items.component.html
@@ -1,7 +1,7 @@