From 472118bfd8642569a24d7e39ff67144725e646db Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Fri, 15 May 2026 14:15:34 -0500 Subject: [PATCH 01/18] PM-35058 Created welcome dialog and dev components --- .../images/access-intelligence/data-is-in.svg | 11 +++ apps/web/src/locales/en/messages.json | 9 ++ .../access-intelligence.module.ts | 2 + .../onboarding/services/onboarding.service.ts | 50 ++++++++++ .../welcome-modal-dialog.component.html | 21 ++++ .../welcome-modal-dialog.component.spec.ts | 31 ++++++ .../welcome-modal-dialog.components.ts | 62 ++++++++++++ .../risk-insights.component.html | 6 ++ .../risk-insights.component.ts | 33 ++++++- .../shared/dev-menu.component.html | 51 ++++++++++ .../shared/dev-menu.component.ts | 97 +++++++++++++++++++ libs/common/src/enums/feature-flag.enum.ts | 2 + libs/state/src/core/state-definitions.ts | 7 ++ 13 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/images/access-intelligence/data-is-in.svg create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts diff --git a/apps/web/src/images/access-intelligence/data-is-in.svg b/apps/web/src/images/access-intelligence/data-is-in.svg new file mode 100644 index 000000000000..49dff21b40d2 --- /dev/null +++ b/apps/web/src/images/access-intelligence/data-is-in.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 6e03e4e7b0a6..04eccaefda7c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -223,6 +223,15 @@ } } }, + "yourDataIsInLetsPutItToWork": { + "message": "Your data is in. Let's put it to work." + }, + "takeAQuickTourOfAccessIntelligence": { + "message": "Take a quick tour of Access Intelligence and see exactly how to turn your org's data into action." + }, + "startTour": { + "message": "Start tour" + }, "noDataInOrgTitle": { "message": "No data found" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 8101dcbc1d0c..8cf846744331 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -22,6 +22,7 @@ import { DefaultAdminTaskService } from "../../vault/services/default-admin-task import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; import { NewApplicationsDialogComponent } from "./activity/application-review-dialog/new-applications-dialog.component"; +import { OnboardingService } from "./onboarding/services/onboarding.service"; import { RiskInsightsComponent } from "./risk-insights.component"; import { AccessIntelligencePageComponent } from "./v2/access-intelligence-page/access-intelligence-page.component"; @@ -69,6 +70,7 @@ import { AccessIntelligencePageComponent } from "./v2/access-intelligence-page/a LogService, ], }), + safeProvider(OnboardingService), ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts new file mode 100644 index 000000000000..9675c338f78e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts @@ -0,0 +1,50 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { + ACCESS_INTELLIGENCE_WELCOME_DIALOG_DISK, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/state"; + +const ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + ACCESS_INTELLIGENCE_WELCOME_DIALOG_DISK, + "accessIntelligenceWelcomeDialogCompleted", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +@Injectable() +export class OnboardingService { + private accountService = inject(AccountService); + private stateProvider = inject(StateProvider); + + async isWelcomeDialogAcknowledged(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + return false; + } + + const acknowledged = await firstValueFrom( + this.stateProvider + .getUserState$(ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY, account.id) + .pipe(map((v) => v ?? false)), + ); + + return acknowledged; + } + + async setWelcomeDialogAcknowledged(value = true) { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (account) { + await this.stateProvider.setUserState( + ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY, + value, + account.id, + ); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.html new file mode 100644 index 000000000000..3b2358440940 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.html @@ -0,0 +1,21 @@ + +
+
+ +
+
+

{{ "yourDataIsInLetsPutItToWork" | i18n }}

+

+ {{ "takeAQuickTourOfAccessIntelligence" | i18n }} +

+
+ + +
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts new file mode 100644 index 000000000000..3dbd8b6993ba --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ButtonModule, DialogModule, TypographyModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { WelcomeModalDialogComponent } from "./welcome-modal-dialog.components"; + +describe("WelcomeModalDialogComponent", () => { + let component: WelcomeModalDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + const mockI18nService = { + t: jest.fn((key: string) => key), + }; + + await TestBed.configureTestingModule({ + imports: [WelcomeModalDialogComponent, TypographyModule, ButtonModule, DialogModule], + providers: [{ provide: I18nService, useValue: mockI18nService }, I18nPipe], + }).compileComponents(); + + fixture = TestBed.createComponent(WelcomeModalDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts new file mode 100644 index 000000000000..d7bd9be73048 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts @@ -0,0 +1,62 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Injector, + runInInjectionContext, +} from "@angular/core"; + +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + TypographyModule, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { OnboardingService } from "./services/onboarding.service"; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-welcome-modal-dialog", + imports: [ButtonModule, TypographyModule, DialogModule, I18nPipe], + templateUrl: "./welcome-modal-dialog.component.html", +}) +export class WelcomeModalDialogComponent { + private dialogRef = inject(DialogRef); + private onboardingService = inject(OnboardingService); + + protected async onStartTour() { + // invoke the dialog here + await this.dialogRef.close(); + } + + protected async onSkip() { + await this.onboardingService + .setWelcomeDialogAcknowledged() + .then(() => { + return this.dialogRef.close(); + }) + .catch(() => {}); + } + + static async showWelcomeDialog( + injector: Injector, + dialogService: DialogService, + ): Promise> { + return runInInjectionContext(injector, async () => { + const onboardingService = inject(OnboardingService); + const acknowledged = await onboardingService.isWelcomeDialogAcknowledged(); + if (acknowledged) { + return Promise.reject("Welcome dialog already acknowledged."); + } + + const dialog = dialogService.open(WelcomeModalDialogComponent, { + width: "600px", + disableClose: true, + }); + return dialog; + }); + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index 7232ae42ff30..2818468539be 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -109,3 +109,9 @@ } } +@if (isDevMode || adoptionUxImprovementsEnabled) { + +} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index 314cb55b5bb3..c5fe96562f4e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -10,6 +10,8 @@ import { inject, signal, ChangeDetectionStrategy, + isDevMode, + Injector, } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; @@ -57,6 +59,8 @@ import { ApplicationsComponent } from "./all-applications/applications.component import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component"; import { EmptyStateCardComponent } from "./empty-state-card.component"; import { RiskInsightsTabType } from "./models/risk-insights.models"; +import { WelcomeModalDialogComponent } from "./onboarding/welcome-modal-dialog.components"; +import { DevMenuComponent } from "./shared/dev-menu.component"; import { PageLoadingComponent } from "./shared/page-loading.component"; import { ReportLoadingComponent } from "./shared/report-loading.component"; import { RiskInsightsDrawerDialogComponent } from "./shared/risk-insights-drawer-dialog.component"; @@ -73,6 +77,7 @@ type ProgressStep = ReportProgress | null; AsyncActionsModule, ButtonModule, CommonModule, + DevMenuComponent, IconModule, CriticalApplicationsComponent, EmptyStateCardComponent, @@ -96,6 +101,8 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { private destroyRef = inject(DestroyRef); protected ReportStatusEnum = ReportStatus; protected milestone11Enabled: boolean = false; + protected adoptionUxImprovementsEnabled: boolean = false; + protected isDevMode = isDevMode(); tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllActivity; @@ -137,6 +144,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { private logService: LogService, private configService: ConfigService, private toastService: ToastService, + private injector: Injector, ) { this.route.queryParams .pipe(takeUntilDestroyed(this.destroyRef)) @@ -176,6 +184,10 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { FeatureFlag.Milestone11AppPageImprovements, ); + this.adoptionUxImprovementsEnabled = await this.configService.getFeatureFlag( + FeatureFlag.AccessIntelligenceAdoptionUxImprovements, + ); + // Subscribe to report data updates // This declarative pattern ensures proper cleanup and prevents memory leaks this.dataService.enrichedReportData$ @@ -266,7 +278,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { }); if (this.invokedFrom()?.source && this.invokedFrom()?.status) { - this.handleReturnParams(this.invokedFrom()?.source, this.invokedFrom()?.status); + await this.handleReturnParams(this.invokedFrom()?.source, this.invokedFrom()?.status); } } @@ -372,11 +384,16 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { } }; - private handleReturnParams(source: string | undefined, status: string | undefined): void { + private async handleReturnParams( + source: string | undefined, + status: string | undefined, + ): Promise { if (source === "import" && status === "success") { this.generateReport(); + await this.beginOnboardingTour(); } + await this.beginOnboardingTour(); this.clearQueryParams(this.router, this.route, ["source", "status"]); } @@ -389,4 +406,16 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { replaceUrl: true, }); } + + protected async beginOnboardingTour(): Promise { + if (this.adoptionUxImprovementsEnabled) { + await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService).catch( + () => { + this.logService.info( + "Welcome dialog did not render. It was already acknowledged or this feature is not available.", + ); + }, + ); + } + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html new file mode 100644 index 000000000000..c6fbdef99c58 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html @@ -0,0 +1,51 @@ +@if (isOpen()) { +
+
+ + Dev Tools + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ Press Esc or click outside to close +
+
+} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts new file mode 100644 index 000000000000..952ebe03f66a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts @@ -0,0 +1,97 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + HostListener, + inject, + isDevMode, + OnInit, + output, + signal, +} from "@angular/core"; + +import { BadgeModule } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; + +import { OnboardingService } from "../onboarding/services/onboarding.service"; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "dirt-dev-menu", + templateUrl: "./dev-menu.component.html", + imports: [BadgeModule], +}) +export class DevMenuComponent implements OnInit { + private readonly elementRef = inject(ElementRef); + private readonly onboardingService = inject(OnboardingService); + private readonly logger = inject(LogService); + protected readonly welcomeDialogAcked = signal(false); + + readonly beginTour = output(); + readonly importData = output(); + protected readonly isOpen = signal(false); + + async ngOnInit(): Promise { + const isAck = await this.onboardingService.isWelcomeDialogAcknowledged(); + this.welcomeDialogAcked.set(isAck); + } + + @HostListener("document:keydown", ["$event"]) + onKeyDown(event: KeyboardEvent): void { + if (!isDevMode()) { + return; + } + if (event.shiftKey && event.key === "?") { + this.isOpen.update((open) => !open); + } else if (event.key === "Escape") { + this.isOpen.set(false); + } + } + + @HostListener("document:click", ["$event"]) + onDocumentClick(event: MouseEvent): void { + if (!isDevMode()) { + return; + } + if (this.isOpen() && !this.elementRef.nativeElement.contains(event.target)) { + this.isOpen.set(false); + } + } + + protected onBeginTour(): void { + this.isOpen.set(false); + this.beginTour.emit(); + // WelcomeModalDialogComponent.showWelcomeDialog(this.dialogService); + } + + protected onImportData(): void { + this.isOpen.set(false); + this.importData.emit(); + } + + protected async onResetWelcomeDialogAck(): Promise { + try { + await this.onboardingService.setWelcomeDialogAcknowledged(false); + this.welcomeDialogAcked.set(false); + this.logger.info("Reset Access Intelligence welcome dialog acknowledged state."); + } catch (error) { + this.logger.error( + "Failed to reset Access Intelligence welcome dialog acknowledged state.", + error, + ); + } + } + + protected async onShowWelcomeDialogAckState(): Promise { + try { + const isAck = await this.onboardingService.isWelcomeDialogAcknowledged(); + this.welcomeDialogAcked.set(isAck); + this.logger.info(`Access Intelligence welcome dialog acknowledged state: ${isAck}.`); + } catch (error) { + this.logger.error( + "Failed to get Access Intelligence welcome dialog acknowledged state.", + error, + ); + } + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 4a8d7d5b9821..8604cd6f7f4c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -71,6 +71,7 @@ export enum FeatureFlag { Milestone11AppPageImprovements = "pm-30538-dirt-milestone-11-app-page-improvements", AccessIntelligenceTrendChart = "pm-26961-access-intelligence-trend-chart", AccessIntelligenceNewArchitecture = "pm-31936-access-intelligence-new-architecture", + AccessIntelligenceAdoptionUxImprovements = "pm-34723-access-intelligence-adoption-ux-improvements", /* Vault */ PM32009NewItemTypes = "pm-32009-new-item-types", @@ -148,6 +149,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.Milestone11AppPageImprovements]: FALSE, [FeatureFlag.AccessIntelligenceTrendChart]: FALSE, [FeatureFlag.AccessIntelligenceNewArchitecture]: FALSE, + [FeatureFlag.AccessIntelligenceAdoptionUxImprovements]: true, /* Vault */ [FeatureFlag.PM32009NewItemTypes]: FALSE, diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 7f9c1931eb2a..1b3f2c9f4b70 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -240,6 +240,13 @@ export const WELCOME_EXTENSION_DIALOG_DISK = new StateDefinition( web: "disk-local", }, ); +export const ACCESS_INTELLIGENCE_WELCOME_DIALOG_DISK = new StateDefinition( + "accessIntelligenceWelcomeDialog", + "disk", + { + web: "disk-local", + }, +); // KM From 773e28cd16d029dfe0b5b33758c4ada567a62854 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Fri, 15 May 2026 14:35:30 -0500 Subject: [PATCH 02/18] PM-35058 v2 integration --- .../risk-insights.component.html | 13 +++++----- .../access-intelligence-page.component.html | 7 ++++++ .../access-intelligence-page.component.ts | 24 +++++++++++++++++++ libs/common/src/enums/feature-flag.enum.ts | 2 +- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index 2818468539be..a5e5605d7ca0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -108,10 +108,11 @@ } } } + + @if (isDevMode || adoptionUxImprovementsEnabled) { + + } -@if (isDevMode || adoptionUxImprovementsEnabled) { - -} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.html index 9ae364831b2d..0e138841dc9b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.html @@ -104,4 +104,11 @@ } } + + @if (isDevMode() || adoptionUxImprovementsEnabled()) { + + } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts index 34f721f39d3c..06fb9c3a6867 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts @@ -9,6 +9,8 @@ import { OnInit, signal, ChangeDetectionStrategy, + Injector, + isDevMode, } from "@angular/core"; import { toObservable, toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; @@ -45,6 +47,8 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { EmptyStateCardComponent } from "../../empty-state-card.component"; import { RiskInsightsTabType } from "../../models/risk-insights.models"; +import { WelcomeModalDialogComponent } from "../../onboarding/welcome-modal-dialog.components"; +import { DevMenuComponent } from "../../shared/dev-menu.component"; import { PageLoadingComponent } from "../../shared/page-loading.component"; import { ReportLoadingComponent } from "../../shared/report-loading.component"; import { ActivityTabComponent } from "../activity-tab/activity-tab.component"; @@ -84,6 +88,7 @@ type ProgressStep = ReportProgress | null; PageLoadingComponent, TabsModule, ReportLoadingComponent, + DevMenuComponent, ], animations: [ trigger("fadeIn", [ @@ -154,6 +159,12 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { protected readonly invokedFrom = signal<{ source: string; status: string } | null>(null); + readonly adoptionUxImprovementsEnabled = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.AccessIntelligenceAdoptionUxImprovements), + ); + + protected readonly isDevMode = signal(isDevMode()); + constructor( private readonly route: ActivatedRoute, private readonly router: Router, @@ -163,6 +174,7 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { private readonly dialogService: DialogService, private readonly logService: LogService, private readonly configService: ConfigService, + private readonly injector: Injector, ) { this.route.queryParams .pipe(takeUntilDestroyed(this.destroyRef)) @@ -428,4 +440,16 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { replaceUrl: true, }); } + + protected async beginOnboardingTour(): Promise { + if (this.adoptionUxImprovementsEnabled()) { + await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService).catch( + () => { + this.logService.info( + "Welcome dialog did not render. It was already acknowledged or this feature is not available.", + ); + }, + ); + } + } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8604cd6f7f4c..2064dd5c0339 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -149,7 +149,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.Milestone11AppPageImprovements]: FALSE, [FeatureFlag.AccessIntelligenceTrendChart]: FALSE, [FeatureFlag.AccessIntelligenceNewArchitecture]: FALSE, - [FeatureFlag.AccessIntelligenceAdoptionUxImprovements]: true, + [FeatureFlag.AccessIntelligenceAdoptionUxImprovements]: FALSE, /* Vault */ [FeatureFlag.PM32009NewItemTypes]: FALSE, From 0762c7a1d444ee7e787ed4a92d3bfd2948043d4a Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Fri, 15 May 2026 14:47:55 -0500 Subject: [PATCH 03/18] PM-35058 invoke the onboarding tour after import is done --- .../access-intelligence-page.component.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts index 06fb9c3a6867..7ef9b2275be7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts @@ -214,7 +214,7 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { }); } - ngOnInit() { + async ngOnInit() { this.route.paramMap .pipe( takeUntilDestroyed(this.destroyRef), @@ -236,7 +236,7 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { void this.currentDialogRef()?.close(); if (this.invokedFrom()?.source && this.invokedFrom()?.status) { - this.handleReturnParams(this.invokedFrom()?.source, this.invokedFrom()?.status); + await this.handleReturnParams(this.invokedFrom()?.source, this.invokedFrom()?.status); } } @@ -423,9 +423,13 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { })); } - private handleReturnParams(source: string | undefined, status: string | undefined): void { + private async handleReturnParams( + source: string | undefined, + status: string | undefined, + ): Promise { if (source === "import" && status === "success") { this.generateReport(); + await this.beginOnboardingTour(); } this.clearQueryParams(this.router, this.route, ["source", "status"]); From 533bf0428987dd6883e480420690352716d22e64 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Fri, 15 May 2026 15:49:53 -0500 Subject: [PATCH 04/18] PM-35058 fix failing test --- .../welcome-modal-dialog.component.spec.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts index 3dbd8b6993ba..f8b459e53ced 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts @@ -1,11 +1,18 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { of } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ButtonModule, DialogModule, TypographyModule } from "@bitwarden/components"; -import { I18nPipe } from "@bitwarden/ui-common"; +import { ButtonModule, DialogModule, DialogRef, TypographyModule } from "@bitwarden/components"; +import { OnboardingService } from "./services/onboarding.service"; import { WelcomeModalDialogComponent } from "./welcome-modal-dialog.components"; +const mockDialogRef = { + close: jest.fn(), + afterClosed: jest.fn().mockReturnValue(of(undefined)), + closed: of(undefined), +} as unknown as import("@bitwarden/components").DialogRef; + describe("WelcomeModalDialogComponent", () => { let component: WelcomeModalDialogComponent; let fixture: ComponentFixture; @@ -14,10 +21,18 @@ describe("WelcomeModalDialogComponent", () => { const mockI18nService = { t: jest.fn((key: string) => key), }; + const mockOnboardingService = { + setWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(undefined), + isWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(false), + }; await TestBed.configureTestingModule({ imports: [WelcomeModalDialogComponent, TypographyModule, ButtonModule, DialogModule], - providers: [{ provide: I18nService, useValue: mockI18nService }, I18nPipe], + providers: [ + { provide: I18nService, useValue: mockI18nService }, + { provide: OnboardingService, useValue: mockOnboardingService }, + { provide: DialogRef, useValue: mockDialogRef }, + ], }).compileComponents(); fixture = TestBed.createComponent(WelcomeModalDialogComponent); From f12d283f1ba34e280084c83c11d7506566729b01 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Mon, 18 May 2026 14:03:02 -0500 Subject: [PATCH 05/18] PM-35058 renamed files and removed unwanted promise.reject --- .../onboarding/welcome-modal-dialog.component.spec.ts | 2 +- ...omponents.ts => welcome-modal-dialog.component.ts} | 11 ++++++++--- .../access-intelligence/risk-insights.component.ts | 10 ++-------- .../access-intelligence-page.component.ts | 10 ++-------- 4 files changed, 13 insertions(+), 20 deletions(-) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-modal-dialog.components.ts => welcome-modal-dialog.component.ts} (79%) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts index f8b459e53ced..8de15435558d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts @@ -5,7 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { ButtonModule, DialogModule, DialogRef, TypographyModule } from "@bitwarden/components"; import { OnboardingService } from "./services/onboarding.service"; -import { WelcomeModalDialogComponent } from "./welcome-modal-dialog.components"; +import { WelcomeModalDialogComponent } from "./welcome-modal-dialog.component"; const mockDialogRef = { close: jest.fn(), diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts similarity index 79% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts index d7bd9be73048..58b280c169c4 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.components.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts @@ -13,6 +13,7 @@ import { DialogService, TypographyModule, } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; import { I18nPipe } from "@bitwarden/ui-common"; import { OnboardingService } from "./services/onboarding.service"; @@ -24,8 +25,8 @@ import { OnboardingService } from "./services/onboarding.service"; templateUrl: "./welcome-modal-dialog.component.html", }) export class WelcomeModalDialogComponent { - private dialogRef = inject(DialogRef); - private onboardingService = inject(OnboardingService); + private readonly dialogRef = inject(DialogRef); + private readonly onboardingService = inject(OnboardingService); protected async onStartTour() { // invoke the dialog here @@ -46,10 +47,14 @@ export class WelcomeModalDialogComponent { dialogService: DialogService, ): Promise> { return runInInjectionContext(injector, async () => { + const logger = inject(LogService); const onboardingService = inject(OnboardingService); const acknowledged = await onboardingService.isWelcomeDialogAcknowledged(); if (acknowledged) { - return Promise.reject("Welcome dialog already acknowledged."); + logger.info( + "[Access Intelligence Onboarding] Welcome dialog already acknowledged, skipping dialog display.", + ); + return; } const dialog = dialogService.open(WelcomeModalDialogComponent, { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index c5fe96562f4e..320f5ac64e7a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -59,7 +59,7 @@ import { ApplicationsComponent } from "./all-applications/applications.component import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component"; import { EmptyStateCardComponent } from "./empty-state-card.component"; import { RiskInsightsTabType } from "./models/risk-insights.models"; -import { WelcomeModalDialogComponent } from "./onboarding/welcome-modal-dialog.components"; +import { WelcomeModalDialogComponent } from "./onboarding/welcome-modal-dialog.component"; import { DevMenuComponent } from "./shared/dev-menu.component"; import { PageLoadingComponent } from "./shared/page-loading.component"; import { ReportLoadingComponent } from "./shared/report-loading.component"; @@ -409,13 +409,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { protected async beginOnboardingTour(): Promise { if (this.adoptionUxImprovementsEnabled) { - await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService).catch( - () => { - this.logService.info( - "Welcome dialog did not render. It was already acknowledged or this feature is not available.", - ); - }, - ); + await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService); } } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts index 7ef9b2275be7..349300435ab3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts @@ -47,7 +47,7 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { EmptyStateCardComponent } from "../../empty-state-card.component"; import { RiskInsightsTabType } from "../../models/risk-insights.models"; -import { WelcomeModalDialogComponent } from "../../onboarding/welcome-modal-dialog.components"; +import { WelcomeModalDialogComponent } from "../../onboarding/welcome-modal-dialog.component"; import { DevMenuComponent } from "../../shared/dev-menu.component"; import { PageLoadingComponent } from "../../shared/page-loading.component"; import { ReportLoadingComponent } from "../../shared/report-loading.component"; @@ -447,13 +447,7 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { protected async beginOnboardingTour(): Promise { if (this.adoptionUxImprovementsEnabled()) { - await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService).catch( - () => { - this.logService.info( - "Welcome dialog did not render. It was already acknowledged or this feature is not available.", - ); - }, - ); + await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService); } } } From f92fdbbdad6bb09d6dc115fb00a1e56643547909 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Mon, 18 May 2026 14:06:56 -0500 Subject: [PATCH 06/18] PM-35058 updated notes on the dev-menu --- .../dirt/access-intelligence/shared/dev-menu.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts index 952ebe03f66a..89c182d92503 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts @@ -15,6 +15,11 @@ import { LogService } from "@bitwarden/logging"; import { OnboardingService } from "../onboarding/services/onboarding.service"; +/*This component is a dev menu only. + * It is not intended for production use and will be removed before release a + * after the feature flag is removed. It is only intended for use in development and testing. + * No language translations are required and therefore no use of i18n pipe or service. + */ @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: "dirt-dev-menu", @@ -61,7 +66,6 @@ export class DevMenuComponent implements OnInit { protected onBeginTour(): void { this.isOpen.set(false); this.beginTour.emit(); - // WelcomeModalDialogComponent.showWelcomeDialog(this.dialogService); } protected onImportData(): void { From 271d6c29aeec67d363204d5ed91623afeb61e77b Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Mon, 18 May 2026 14:46:16 -0500 Subject: [PATCH 07/18] PM-35058 fixed type test failure --- .../onboarding/welcome-modal-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts index 58b280c169c4..c24197789d57 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts @@ -45,7 +45,7 @@ export class WelcomeModalDialogComponent { static async showWelcomeDialog( injector: Injector, dialogService: DialogService, - ): Promise> { + ): Promise | undefined> { return runInInjectionContext(injector, async () => { const logger = inject(LogService); const onboardingService = inject(OnboardingService); From 65048cbda235676764742d8d3338299c1d68b428 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Tue, 19 May 2026 11:43:30 -0500 Subject: [PATCH 08/18] PM-34724 welcome carousel dialog for new admins --- apps/web/src/locales/en/messages.json | 27 ++++ .../services/onboarding.service.spec.ts | 99 +++++++++++++++ .../onboarding/services/onboarding.service.ts | 35 +++++- .../welcome-carousel-dialog.component.html | 116 +++++++++++++++++ .../welcome-carousel-dialog.component.spec.ts | 117 ++++++++++++++++++ ...lcome-carousel-dialog.component.stories.ts | 41 ++++++ .../welcome-carousel-dialog.component.ts | 68 ++++++++++ .../welcome-modal-dialog.component.spec.ts | 45 +++++-- .../welcome-modal-dialog.component.ts | 15 ++- .../risk-insights.component.html | 1 + .../risk-insights.component.ts | 29 ++++- .../shared/dev-menu.component.html | 28 +++++ .../shared/dev-menu.component.ts | 42 ++++++- .../access-intelligence-page.component.ts | 6 +- .../carousel/carousel.component.html | 45 ++++--- .../carousel/carousel.component.spec.ts | 60 +++++++++ .../components/carousel/carousel.component.ts | 13 +- .../components/carousel/carousel.stories.ts | 26 ++++ 18 files changed, 772 insertions(+), 41 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.stories.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 04eccaefda7c..16d91b2ce44f 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -229,6 +229,33 @@ "takeAQuickTourOfAccessIntelligence": { "message": "Take a quick tour of Access Intelligence and see exactly how to turn your org's data into action." }, + "yourEntireOrgsSecurityInOneView": { + "message": "Your entire org's security, in one view" + }, + "accessIntelligenceGivesYouSinglePlace": { + "message": "Access Intelligence gives you a single place to see every app, password, and member that's at risk." + }, + "youSetThePrioritiesWeSurfaceTheRisks": { + "message": "You set the priorities. We surface the risks." + }, + "youMarkWhichAppsAreMostCritical": { + "message": "You mark which apps are most critical, and Access Intelligence automatically identifies the at-risk passwords that need attention first." + }, + "trackImprovementsAcrossYourTeam": { + "message": "Track improvements across your team" + }, + "membersAreAutomaticallyNotified": { + "message": "Members are automatically notified of at-risk passwords in their Bitwarden extension. No chasing required." + }, + "importYourOrgDataToGetStarted": { + "message": "Import your org data to get started" + }, + "onceItHasTheVaultData": { + "message": "Once it has the vault data, Access Intelligence will have everything it needs to report on your org's password health." + }, + "accessIntelligenceWelcomeTour": { + "message": "Access Intelligence welcome tour" + }, "startTour": { "message": "Start tour" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts new file mode 100644 index 000000000000..193c732fcaa6 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts @@ -0,0 +1,99 @@ +import { TestBed } from "@angular/core/testing"; +import { of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { StateProvider } from "@bitwarden/state"; + +import { OnboardingService } from "./onboarding.service"; + +const mockAccount = { id: "test-user-id-123" }; + +describe("OnboardingService", () => { + let service: OnboardingService; + let mockStateProvider: { getUserState$: jest.Mock; setUserState: jest.Mock }; + + beforeEach(async () => { + mockStateProvider = { + getUserState$: jest.fn().mockReturnValue(of(null)), + setUserState: jest.fn().mockResolvedValue(undefined), + }; + + await TestBed.configureTestingModule({ + providers: [ + OnboardingService, + { provide: AccountService, useValue: { activeAccount$: of(mockAccount) } }, + { provide: StateProvider, useValue: mockStateProvider }, + ], + }); + + service = TestBed.inject(OnboardingService); + }); + + describe("isCarouselAcknowledged", () => { + it("returns false when state is null", async () => { + mockStateProvider.getUserState$.mockReturnValue(of(null)); + const result = await service.isCarouselAcknowledged(); + expect(result).toBe(false); + }); + + it("returns false when state is false", async () => { + mockStateProvider.getUserState$.mockReturnValue(of(false)); + const result = await service.isCarouselAcknowledged(); + expect(result).toBe(false); + }); + + it("returns true when state is true", async () => { + mockStateProvider.getUserState$.mockReturnValue(of(true)); + const result = await service.isCarouselAcknowledged(); + expect(result).toBe(true); + }); + + it("returns false when there is no active account", async () => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + providers: [ + OnboardingService, + { provide: AccountService, useValue: { activeAccount$: of(null) } }, + { provide: StateProvider, useValue: mockStateProvider }, + ], + }); + const noAccountService = TestBed.inject(OnboardingService); + const result = await noAccountService.isCarouselAcknowledged(); + expect(result).toBe(false); + }); + }); + + describe("setCarouselAcknowledged", () => { + it("calls setUserState with true by default", async () => { + await service.setCarouselAcknowledged(); + expect(mockStateProvider.setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "accessIntelligenceCarouselAcknowledged" }), + true, + mockAccount.id, + ); + }); + + it("calls setUserState with the provided value", async () => { + await service.setCarouselAcknowledged(false); + expect(mockStateProvider.setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "accessIntelligenceCarouselAcknowledged" }), + false, + mockAccount.id, + ); + }); + + it("does not call setUserState when there is no active account", async () => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + providers: [ + OnboardingService, + { provide: AccountService, useValue: { activeAccount$: of(null) } }, + { provide: StateProvider, useValue: mockStateProvider }, + ], + }); + const noAccountService = TestBed.inject(OnboardingService); + await noAccountService.setCarouselAcknowledged(); + expect(mockStateProvider.setUserState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts index 9675c338f78e..7820008d5568 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts @@ -13,7 +13,16 @@ const ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinitio "accessIntelligenceWelcomeDialogCompleted", { deserializer: (value) => value, - clearOn: [], + clearOn: [], // Welcome dialog acknowledged state should persist across lock/logout so the dialog is not reshown + }, +); + +const ACCESS_INTELLIGENCE_CAROUSEL_ACKNOWLEDGED_KEY = new UserKeyDefinition( + ACCESS_INTELLIGENCE_WELCOME_DIALOG_DISK, + "accessIntelligenceCarouselAcknowledged", + { + deserializer: (value) => value, + clearOn: [], // Carousel acknowledged state should persist across lock/logout so the tour is not reshown }, ); @@ -47,4 +56,28 @@ export class OnboardingService { ); } } + + async isCarouselAcknowledged(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + return false; + } + + return await firstValueFrom( + this.stateProvider + .getUserState$(ACCESS_INTELLIGENCE_CAROUSEL_ACKNOWLEDGED_KEY, account.id) + .pipe(map((v) => v ?? false)), + ); + } + + async setCarouselAcknowledged(value = true) { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (account) { + await this.stateProvider.setUserState( + ACCESS_INTELLIGENCE_CAROUSEL_ACKNOWLEDGED_KEY, + value, + account.id, + ); + } + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.html new file mode 100644 index 000000000000..9aaf3d5e9278 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.html @@ -0,0 +1,116 @@ + +
+ + + +
+ +

{{ "yourEntireOrgsSecurityInOneView" | i18n }}

+

{{ "accessIntelligenceGivesYouSinglePlace" | i18n }}

+
+
+ + + +
+ +

{{ "youSetThePrioritiesWeSurfaceTheRisks" | i18n }}

+

{{ "youMarkWhichAppsAreMostCritical" | i18n }}

+
+
+ + + +
+ +

{{ "trackImprovementsAcrossYourTeam" | i18n }}

+

{{ "membersAreAutomaticallyNotified" | i18n }}

+
+
+ + + +
+ +

{{ "importYourOrgDataToGetStarted" | i18n }}

+

{{ "onceItHasTheVaultData" | i18n }}

+
+
+ + +
+ @if (isFirstSlide()) { + + } @else { + + } + + @if (isLastSlide()) { + + } @else { + + } +
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts new file mode 100644 index 000000000000..1cbf4fac88d2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts @@ -0,0 +1,117 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { of } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { ButtonModule, DialogModule, DialogRef, DIALOG_DATA } from "@bitwarden/components"; +import { VaultCarouselModule } from "@bitwarden/vault"; + +import { OnboardingService } from "./services/onboarding.service"; +import { WelcomeCarouselDialogComponent } from "./welcome-carousel-dialog.component"; + +const mockOrganizationId = "test-org-id" as OrganizationId; + +const mockDialogRef = { + close: jest.fn(), + afterClosed: jest.fn().mockReturnValue(of(undefined)), + closed: of(undefined), +} as unknown as DialogRef; + +const mockOnboardingService = { + setCarouselAcknowledged: jest.fn().mockResolvedValue(undefined), +}; + +const mockRouter = { + navigate: jest.fn().mockResolvedValue(true), +}; + +describe("WelcomeCarouselDialogComponent", () => { + let component: WelcomeCarouselDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + jest.clearAllMocks(); + + await TestBed.configureTestingModule({ + imports: [WelcomeCarouselDialogComponent, VaultCarouselModule, DialogModule, ButtonModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DialogRef, useValue: mockDialogRef }, + { provide: OnboardingService, useValue: mockOnboardingService }, + { provide: Router, useValue: mockRouter }, + { provide: DIALOG_DATA, useValue: { organizationId: mockOrganizationId } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(WelcomeCarouselDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("starts on the first slide", () => { + expect(component["currentSlide"]()).toBe(0); + expect(component["isFirstSlide"]()).toBe(true); + expect(component["isLastSlide"]()).toBe(false); + }); + + it("onSlideChange updates the currentSlide signal", () => { + component["onSlideChange"](2); + expect(component["currentSlide"]()).toBe(2); + }); + + it("isLastSlide is true when on slide 3 (index 3)", () => { + component["onSlideChange"](3); + expect(component["isLastSlide"]()).toBe(true); + expect(component["isFirstSlide"]()).toBe(false); + }); + + describe("onSkip", () => { + it("calls setCarouselAcknowledged", async () => { + await component["onSkip"](); + expect(mockOnboardingService.setCarouselAcknowledged).toHaveBeenCalledTimes(1); + }); + + it("closes the dialog", async () => { + await component["onSkip"](); + expect(mockDialogRef.close).toHaveBeenCalledTimes(1); + }); + }); + + describe("onImportData", () => { + it("calls setCarouselAcknowledged", async () => { + await component["onImportData"](); + expect(mockOnboardingService.setCarouselAcknowledged).toHaveBeenCalledTimes(1); + }); + + it("navigates to the org import page", async () => { + await component["onImportData"](); + expect(mockRouter.navigate).toHaveBeenCalledWith( + ["/organizations", mockOrganizationId, "settings", "tools", "import"], + { queryParams: { returnTo: "access-intelligence" } }, + ); + }); + + it("closes the dialog", async () => { + await component["onImportData"](); + expect(mockDialogRef.close).toHaveBeenCalledTimes(1); + }); + + it("closes the dialog before navigating", async () => { + const callOrder: string[] = []; + (mockDialogRef.close as jest.Mock).mockImplementation(() => callOrder.push("close")); + mockRouter.navigate.mockImplementation(async () => { + callOrder.push("navigate"); + return true; + }); + + await component["onImportData"](); + + expect(callOrder).toEqual(["close", "navigate"]); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.stories.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.stories.ts new file mode 100644 index 000000000000..8ddb89fe9a51 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.stories.ts @@ -0,0 +1,41 @@ +import { Router } from "@angular/router"; +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + ButtonModule, + DialogModule, + DialogRef, + DIALOG_DATA, + TypographyModule, +} from "@bitwarden/components"; +import { VaultCarouselModule } from "@bitwarden/vault"; + +import { OnboardingService } from "./services/onboarding.service"; +import { WelcomeCarouselDialogComponent } from "./welcome-carousel-dialog.component"; + +const mockDialogRef = { close: async () => {} }; +const mockOnboardingService = { setCarouselAcknowledged: async () => {} }; +const mockOrganizationId = "story-org-id" as OrganizationId; + +export default { + title: "Access Intelligence/WelcomeCarouselDialog", + component: WelcomeCarouselDialogComponent, + decorators: [ + moduleMetadata({ + imports: [VaultCarouselModule, DialogModule, ButtonModule, TypographyModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DialogRef, useValue: mockDialogRef }, + { provide: OnboardingService, useValue: mockOnboardingService }, + { provide: DIALOG_DATA, useValue: { organizationId: mockOrganizationId } }, + { provide: Router, useValue: { navigate: async () => {} } }, + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts new file mode 100644 index 000000000000..52bcae083faf --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts @@ -0,0 +1,68 @@ +import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core"; +import { Router } from "@angular/router"; + +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + DIALOG_DATA, + TypographyModule, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { VaultCarouselModule } from "@bitwarden/vault"; + +import { OnboardingService } from "./services/onboarding.service"; + +export type WelcomeCarouselDialogData = { + organizationId: OrganizationId; +}; + +const TOTAL_SLIDES = 4; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-welcome-carousel-dialog", + imports: [ButtonModule, DialogModule, I18nPipe, TypographyModule, VaultCarouselModule], + templateUrl: "./welcome-carousel-dialog.component.html", +}) +export class WelcomeCarouselDialogComponent { + private readonly dialogRef = inject(DialogRef); + private readonly router = inject(Router); + private readonly onboardingService = inject(OnboardingService); + private readonly data = inject(DIALOG_DATA); + + protected readonly currentSlide = signal(0); + protected readonly isFirstSlide = computed(() => this.currentSlide() === 0); + protected readonly isLastSlide = computed(() => this.currentSlide() === TOTAL_SLIDES - 1); + + protected onSlideChange(index: number): void { + this.currentSlide.set(index); + } + + protected async onSkip(): Promise { + await this.onboardingService.setCarouselAcknowledged(); + await this.dialogRef.close(); + } + + protected async onImportData(): Promise { + await this.onboardingService.setCarouselAcknowledged(); + await this.dialogRef.close(); + await this.router.navigate( + ["/organizations", this.data.organizationId, "settings", "tools", "import"], + { queryParams: { returnTo: "access-intelligence" } }, + ); + } + + static open( + dialogService: DialogService, + organizationId: OrganizationId, + ): DialogRef { + return dialogService.open(WelcomeCarouselDialogComponent, { + data: { organizationId } satisfies WelcomeCarouselDialogData, + width: "600px", + disableClose: true, + }); + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts index 8de15435558d..badedd60e13a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts @@ -2,36 +2,51 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { of } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ButtonModule, DialogModule, DialogRef, TypographyModule } from "@bitwarden/components"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + TypographyModule, + DIALOG_DATA, +} from "@bitwarden/components"; import { OnboardingService } from "./services/onboarding.service"; import { WelcomeModalDialogComponent } from "./welcome-modal-dialog.component"; +const mockOrganizationId = "test-org-id" as OrganizationId; + const mockDialogRef = { - close: jest.fn(), + close: jest.fn().mockResolvedValue(undefined), afterClosed: jest.fn().mockReturnValue(of(undefined)), closed: of(undefined), -} as unknown as import("@bitwarden/components").DialogRef; +} as unknown as DialogRef; + +const mockDialogService = { + open: jest.fn().mockReturnValue(mockDialogRef), +}; + +const mockOnboardingService = { + setWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(undefined), + isWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(false), +}; describe("WelcomeModalDialogComponent", () => { let component: WelcomeModalDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { - const mockI18nService = { - t: jest.fn((key: string) => key), - }; - const mockOnboardingService = { - setWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(undefined), - isWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(false), - }; + jest.clearAllMocks(); await TestBed.configureTestingModule({ imports: [WelcomeModalDialogComponent, TypographyModule, ButtonModule, DialogModule], providers: [ - { provide: I18nService, useValue: mockI18nService }, + { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: OnboardingService, useValue: mockOnboardingService }, { provide: DialogRef, useValue: mockDialogRef }, + { provide: DialogService, useValue: mockDialogService }, + { provide: DIALOG_DATA, useValue: { organizationId: mockOrganizationId } }, ], }).compileComponents(); @@ -43,4 +58,12 @@ describe("WelcomeModalDialogComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + describe("onSkip", () => { + it("calls setWelcomeDialogAcknowledged and closes the dialog", async () => { + await component["onSkip"](); + expect(mockOnboardingService.setWelcomeDialogAcknowledged).toHaveBeenCalled(); + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts index c24197789d57..49116a7ea35c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts @@ -6,18 +6,24 @@ import { runInInjectionContext, } from "@angular/core"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { ButtonModule, DialogModule, DialogRef, DialogService, TypographyModule, + DIALOG_DATA, } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; import { I18nPipe } from "@bitwarden/ui-common"; import { OnboardingService } from "./services/onboarding.service"; +export type WelcomeModalDialogData = { + organizationId: OrganizationId; +}; + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: "app-welcome-modal-dialog", @@ -26,14 +32,15 @@ import { OnboardingService } from "./services/onboarding.service"; }) export class WelcomeModalDialogComponent { private readonly dialogRef = inject(DialogRef); + private readonly dialogService = inject(DialogService); private readonly onboardingService = inject(OnboardingService); + private readonly data = inject(DIALOG_DATA); - protected async onStartTour() { - // invoke the dialog here + protected async onStartTour(): Promise { await this.dialogRef.close(); } - protected async onSkip() { + protected async onSkip(): Promise { await this.onboardingService .setWelcomeDialogAcknowledged() .then(() => { @@ -45,6 +52,7 @@ export class WelcomeModalDialogComponent { static async showWelcomeDialog( injector: Injector, dialogService: DialogService, + organizationId: OrganizationId, ): Promise | undefined> { return runInInjectionContext(injector, async () => { const logger = inject(LogService); @@ -58,6 +66,7 @@ export class WelcomeModalDialogComponent { } const dialog = dialogService.open(WelcomeModalDialogComponent, { + data: { organizationId } satisfies WelcomeModalDialogData, width: "600px", disableClose: true, }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index a5e5605d7ca0..04feee053648 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -112,6 +112,7 @@ @if (isDevMode || adoptionUxImprovementsEnabled) { } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index 320f5ac64e7a..db4073014f14 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -15,7 +15,7 @@ import { } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { concat, EMPTY, firstValueFrom, of } from "rxjs"; +import { combineLatest, concat, EMPTY, firstValueFrom, of } from "rxjs"; import { concatMap, delay, @@ -24,6 +24,7 @@ import { map, skip, switchMap, + take, tap, } from "rxjs/operators"; @@ -59,6 +60,7 @@ import { ApplicationsComponent } from "./all-applications/applications.component import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component"; import { EmptyStateCardComponent } from "./empty-state-card.component"; import { RiskInsightsTabType } from "./models/risk-insights.models"; +import { WelcomeCarouselDialogComponent } from "./onboarding/welcome-carousel-dialog.component"; import { WelcomeModalDialogComponent } from "./onboarding/welcome-modal-dialog.component"; import { DevMenuComponent } from "./shared/dev-menu.component"; import { PageLoadingComponent } from "./shared/page-loading.component"; @@ -277,6 +279,18 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { this.currentProgressStep.set(step); }); + combineLatest([this.dataService.hasReportData$, this.dataService.hasCiphers$]) + .pipe( + takeUntilDestroyed(this.destroyRef), + filter(([hasReportData, hasCiphers]) => !hasReportData && !hasCiphers), + take(1), + ) + .subscribe(() => { + void this.launchOnboardingWelcome().catch((error: unknown) => { + this.logService.error("Failed to launch onboarding welcome", error); + }); + }); + if (this.invokedFrom()?.source && this.invokedFrom()?.status) { await this.handleReturnParams(this.invokedFrom()?.source, this.invokedFrom()?.status); } @@ -393,7 +407,6 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { await this.beginOnboardingTour(); } - await this.beginOnboardingTour(); this.clearQueryParams(this.router, this.route, ["source", "status"]); } @@ -409,7 +422,17 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { protected async beginOnboardingTour(): Promise { if (this.adoptionUxImprovementsEnabled) { - await WelcomeModalDialogComponent.showWelcomeDialog(this.injector, this.dialogService); + await WelcomeModalDialogComponent.showWelcomeDialog( + this.injector, + this.dialogService, + this.organizationId, + ); + } + } + + protected async launchOnboardingWelcome(): Promise { + if (this.adoptionUxImprovementsEnabled) { + await WelcomeCarouselDialogComponent.open(this.dialogService, this.organizationId); } } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html index c6fbdef99c58..695f81c694d5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html @@ -16,6 +16,34 @@ Import Data from Access Intelligence +
+ +
+
+ +
+
+ +
+ + @if (!hideArrows) { + + } +
}
- + + @if (!hideArrows) { + + } +
+ @if (minHeight === null) {
diff --git a/libs/vault/src/components/carousel/carousel.component.spec.ts b/libs/vault/src/components/carousel/carousel.component.spec.ts index eb9480398e9b..7f058d9ebb2e 100644 --- a/libs/vault/src/components/carousel/carousel.component.spec.ts +++ b/libs/vault/src/components/carousel/carousel.component.spec.ts @@ -100,3 +100,63 @@ describe("VaultCarouselComponent", () => { expect(component.slideChange.emit).toHaveBeenCalledWith(0); }); }); + +@Component({ + selector: "app-test-carousel-hide-arrows", + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +

Content 1

+

Content 2

+
+ +
+
+ `, +}) +class TestCarouselHideArrowsComponent {} + +describe("VaultCarouselComponent with hideArrows", () => { + let fixture: ComponentFixture; + let carouselComponent: VaultCarouselComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCarouselHideArrowsComponent); + fixture.detectChanges(); + carouselComponent = fixture.debugElement.query( + By.directive(VaultCarouselComponent), + ).componentInstance; + }); + + it("hides navigation arrow buttons when hideArrows is true", () => { + const iconButtons = fixture.debugElement.queryAll(By.css("[bitIconButton]")); + expect(iconButtons.length).toBe(0); + }); + + it("renders carouselActions slot content", () => { + const actionBtn = fixture.debugElement.query(By.css("[data-testid='action-btn']")); + expect(actionBtn).not.toBeNull(); + }); + + it("nextSlide advances to the next slide", () => { + const slideChangeSpy = jest.spyOn(carouselComponent.slideChange, "emit"); + carouselComponent.nextSlide(); + fixture.detectChanges(); + expect(slideChangeSpy).toHaveBeenCalledWith(1); + }); + + it("prevSlide goes back to the previous slide after advancing", () => { + carouselComponent.nextSlide(); + fixture.detectChanges(); + const slideChangeSpy = jest.spyOn(carouselComponent.slideChange, "emit"); + carouselComponent.prevSlide(); + fixture.detectChanges(); + expect(slideChangeSpy).toHaveBeenCalledWith(0); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts index c622f2e5d855..788d03a2235d 100644 --- a/libs/vault/src/components/carousel/carousel.component.ts +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -3,6 +3,7 @@ import { CdkPortalOutlet } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, ContentChildren, @@ -54,6 +55,14 @@ export class VaultCarouselComponent implements AfterViewInit { // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) label = ""; + /** + * When true, hides the previous/next arrow navigation buttons. + * Use with the `carouselActions` content slot to provide custom navigation controls. + */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input({ transform: booleanAttribute }) hideArrows = false; + /** * Emits the index of the newly selected slide. */ @@ -118,13 +127,13 @@ export class VaultCarouselComponent implements AfterViewInit { this.slideChange.emit(index); } - protected nextSlide() { + nextSlide() { if (this.selectedIndex < this.slides.length - 1) { this.selectSlide(this.selectedIndex + 1); } } - protected prevSlide() { + prevSlide() { if (this.selectedIndex > 0) { this.selectSlide(this.selectedIndex - 1); } diff --git a/libs/vault/src/components/carousel/carousel.stories.ts b/libs/vault/src/components/carousel/carousel.stories.ts index 1e393779a6a1..a839502fe6d4 100644 --- a/libs/vault/src/components/carousel/carousel.stories.ts +++ b/libs/vault/src/components/carousel/carousel.stories.ts @@ -76,3 +76,29 @@ export const KeyboardNavigation: Story = { `, }), }; + +export const HideArrowsWithActions: Story = { + render: (args: any) => ({ + props: args, + template: ` + + +
+

Slide Without Arrows

+

Dots only — no navigation arrows

+
+
+ +
+

Second Slide

+

Buttons projected below the dots via carouselActions slot

+
+
+
+ + +
+
+ `, + }), +}; From aba9fe3e18769cd9bd1499568d7c35d3b502b421 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Tue, 19 May 2026 13:14:01 -0500 Subject: [PATCH 09/18] PM-34724 renamed components to be easily understood --- ...> new-admin-welcome-dialog.component.html} | 0 ...ew-admin-welcome-dialog.component.spec.ts} | 22 ++++----- ...admin-welcome-dialog.component.stories.ts} | 0 ... => new-admin-welcome-dialog.component.ts} | 49 ++++++++++++++----- ...> post-import-modal-dialog.component.html} | 0 ...ost-import-modal-dialog.component.spec.ts} | 20 ++++---- ... => post-import-modal-dialog.component.ts} | 16 +++--- .../services/onboarding.service.spec.ts | 26 +++++----- .../onboarding/services/onboarding.service.ts | 28 +++++------ .../risk-insights.component.ts | 12 +++-- .../shared/dev-menu.component.ts | 13 ++--- .../access-intelligence-page.component.ts | 4 +- 12 files changed, 109 insertions(+), 81 deletions(-) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-carousel-dialog.component.html => new-admin-welcome-dialog.component.html} (100%) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-carousel-dialog.component.spec.ts => new-admin-welcome-dialog.component.spec.ts} (80%) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-carousel-dialog.component.stories.ts => new-admin-welcome-dialog.component.stories.ts} (100%) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-carousel-dialog.component.ts => new-admin-welcome-dialog.component.ts} (54%) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-modal-dialog.component.html => post-import-modal-dialog.component.html} (100%) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-modal-dialog.component.spec.ts => post-import-modal-dialog.component.spec.ts} (68%) rename bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/{welcome-modal-dialog.component.ts => post-import-modal-dialog.component.ts} (77%) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.html rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.html diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.spec.ts similarity index 80% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.spec.ts index 1cbf4fac88d2..bf6324de4500 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.spec.ts @@ -7,8 +7,8 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { ButtonModule, DialogModule, DialogRef, DIALOG_DATA } from "@bitwarden/components"; import { VaultCarouselModule } from "@bitwarden/vault"; +import { NewAdminWelcomeDialogComponent } from "./new-admin-welcome-dialog.component"; import { OnboardingService } from "./services/onboarding.service"; -import { WelcomeCarouselDialogComponent } from "./welcome-carousel-dialog.component"; const mockOrganizationId = "test-org-id" as OrganizationId; @@ -19,22 +19,22 @@ const mockDialogRef = { } as unknown as DialogRef; const mockOnboardingService = { - setCarouselAcknowledged: jest.fn().mockResolvedValue(undefined), + setNewAdminWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(undefined), }; const mockRouter = { navigate: jest.fn().mockResolvedValue(true), }; -describe("WelcomeCarouselDialogComponent", () => { - let component: WelcomeCarouselDialogComponent; - let fixture: ComponentFixture; +describe("NewAdminWelcomeDialogComponent", () => { + let component: NewAdminWelcomeDialogComponent; + let fixture: ComponentFixture; beforeEach(async () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ - imports: [WelcomeCarouselDialogComponent, VaultCarouselModule, DialogModule, ButtonModule], + imports: [NewAdminWelcomeDialogComponent, VaultCarouselModule, DialogModule, ButtonModule], providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: DialogRef, useValue: mockDialogRef }, @@ -44,7 +44,7 @@ describe("WelcomeCarouselDialogComponent", () => { ], }).compileComponents(); - fixture = TestBed.createComponent(WelcomeCarouselDialogComponent); + fixture = TestBed.createComponent(NewAdminWelcomeDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -71,9 +71,9 @@ describe("WelcomeCarouselDialogComponent", () => { }); describe("onSkip", () => { - it("calls setCarouselAcknowledged", async () => { + it("calls setNewAdminWelcomeDialogAcknowledged", async () => { await component["onSkip"](); - expect(mockOnboardingService.setCarouselAcknowledged).toHaveBeenCalledTimes(1); + expect(mockOnboardingService.setNewAdminWelcomeDialogAcknowledged).toHaveBeenCalledTimes(1); }); it("closes the dialog", async () => { @@ -83,9 +83,9 @@ describe("WelcomeCarouselDialogComponent", () => { }); describe("onImportData", () => { - it("calls setCarouselAcknowledged", async () => { + it("calls setNewAdminWelcomeDialogAcknowledged", async () => { await component["onImportData"](); - expect(mockOnboardingService.setCarouselAcknowledged).toHaveBeenCalledTimes(1); + expect(mockOnboardingService.setNewAdminWelcomeDialogAcknowledged).toHaveBeenCalledTimes(1); }); it("navigates to the org import page", async () => { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.stories.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts similarity index 100% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.stories.ts rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts similarity index 54% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts index 52bcae083faf..2708e16fccba 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-carousel-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + Injector, + runInInjectionContext, + signal, +} from "@angular/core"; import { Router } from "@angular/router"; import { OrganizationId } from "@bitwarden/common/types/guid"; @@ -10,6 +18,7 @@ import { DIALOG_DATA, TypographyModule, } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; import { I18nPipe } from "@bitwarden/ui-common"; import { VaultCarouselModule } from "@bitwarden/vault"; @@ -23,12 +32,12 @@ const TOTAL_SLIDES = 4; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - selector: "app-welcome-carousel-dialog", + selector: "app-new-admin-welcome-dialog", imports: [ButtonModule, DialogModule, I18nPipe, TypographyModule, VaultCarouselModule], - templateUrl: "./welcome-carousel-dialog.component.html", + templateUrl: "./new-admin-welcome-dialog.component.html", }) -export class WelcomeCarouselDialogComponent { - private readonly dialogRef = inject(DialogRef); +export class NewAdminWelcomeDialogComponent { + private readonly dialogRef = inject(DialogRef); private readonly router = inject(Router); private readonly onboardingService = inject(OnboardingService); private readonly data = inject(DIALOG_DATA); @@ -42,12 +51,12 @@ export class WelcomeCarouselDialogComponent { } protected async onSkip(): Promise { - await this.onboardingService.setCarouselAcknowledged(); + await this.onboardingService.setNewAdminWelcomeDialogAcknowledged(); await this.dialogRef.close(); } protected async onImportData(): Promise { - await this.onboardingService.setCarouselAcknowledged(); + await this.onboardingService.setNewAdminWelcomeDialogAcknowledged(); await this.dialogRef.close(); await this.router.navigate( ["/organizations", this.data.organizationId, "settings", "tools", "import"], @@ -55,14 +64,28 @@ export class WelcomeCarouselDialogComponent { ); } - static open( + static async showWelcomeCarouselDialog( + injector: Injector, dialogService: DialogService, organizationId: OrganizationId, - ): DialogRef { - return dialogService.open(WelcomeCarouselDialogComponent, { - data: { organizationId } satisfies WelcomeCarouselDialogData, - width: "600px", - disableClose: true, + ): Promise | undefined> { + return runInInjectionContext(injector, async () => { + const logger = inject(LogService); + const onboardingService = inject(OnboardingService); + const acknowledged = await onboardingService.isNewAdminWelcomeDialogAcknowledged(); + if (acknowledged) { + logger.info( + "[Access Intelligence Onboarding] Welcome carousel already acknowledged, skipping dialog display.", + ); + return; + } + + const dialog = dialogService.open(NewAdminWelcomeDialogComponent, { + data: { organizationId } satisfies WelcomeCarouselDialogData, + width: "600px", + disableClose: true, + }); + return dialog; }); } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.html rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.html diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.spec.ts similarity index 68% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.spec.ts index badedd60e13a..4904e7e000af 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.spec.ts @@ -12,8 +12,8 @@ import { DIALOG_DATA, } from "@bitwarden/components"; +import { PostImportModalDialogComponent } from "./post-import-modal-dialog.component"; import { OnboardingService } from "./services/onboarding.service"; -import { WelcomeModalDialogComponent } from "./welcome-modal-dialog.component"; const mockOrganizationId = "test-org-id" as OrganizationId; @@ -28,19 +28,19 @@ const mockDialogService = { }; const mockOnboardingService = { - setWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(undefined), - isWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(false), + setPostImportDialogAcknowledged: jest.fn().mockResolvedValue(undefined), + isPostImportDialogAcknowledged: jest.fn().mockResolvedValue(false), }; -describe("WelcomeModalDialogComponent", () => { - let component: WelcomeModalDialogComponent; - let fixture: ComponentFixture; +describe("PostImportModalDialogComponent", () => { + let component: PostImportModalDialogComponent; + let fixture: ComponentFixture; beforeEach(async () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ - imports: [WelcomeModalDialogComponent, TypographyModule, ButtonModule, DialogModule], + imports: [PostImportModalDialogComponent, TypographyModule, ButtonModule, DialogModule], providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: OnboardingService, useValue: mockOnboardingService }, @@ -50,7 +50,7 @@ describe("WelcomeModalDialogComponent", () => { ], }).compileComponents(); - fixture = TestBed.createComponent(WelcomeModalDialogComponent); + fixture = TestBed.createComponent(PostImportModalDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -60,9 +60,9 @@ describe("WelcomeModalDialogComponent", () => { }); describe("onSkip", () => { - it("calls setWelcomeDialogAcknowledged and closes the dialog", async () => { + it("calls setPostImportDialogAcknowledged and closes the dialog", async () => { await component["onSkip"](); - expect(mockOnboardingService.setWelcomeDialogAcknowledged).toHaveBeenCalled(); + expect(mockOnboardingService.setPostImportDialogAcknowledged).toHaveBeenCalled(); expect(mockDialogRef.close).toHaveBeenCalled(); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts similarity index 77% rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts index 49116a7ea35c..446f1bf157f1 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/welcome-modal-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts @@ -26,12 +26,12 @@ export type WelcomeModalDialogData = { @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - selector: "app-welcome-modal-dialog", + selector: "app-post-import-modal-dialog", imports: [ButtonModule, TypographyModule, DialogModule, I18nPipe], - templateUrl: "./welcome-modal-dialog.component.html", + templateUrl: "./post-import-modal-dialog.component.html", }) -export class WelcomeModalDialogComponent { - private readonly dialogRef = inject(DialogRef); +export class PostImportModalDialogComponent { + private readonly dialogRef = inject(DialogRef); private readonly dialogService = inject(DialogService); private readonly onboardingService = inject(OnboardingService); private readonly data = inject(DIALOG_DATA); @@ -42,7 +42,7 @@ export class WelcomeModalDialogComponent { protected async onSkip(): Promise { await this.onboardingService - .setWelcomeDialogAcknowledged() + .setPostImportDialogAcknowledged() .then(() => { return this.dialogRef.close(); }) @@ -53,11 +53,11 @@ export class WelcomeModalDialogComponent { injector: Injector, dialogService: DialogService, organizationId: OrganizationId, - ): Promise | undefined> { + ): Promise | undefined> { return runInInjectionContext(injector, async () => { const logger = inject(LogService); const onboardingService = inject(OnboardingService); - const acknowledged = await onboardingService.isWelcomeDialogAcknowledged(); + const acknowledged = await onboardingService.isPostImportDialogAcknowledged(); if (acknowledged) { logger.info( "[Access Intelligence Onboarding] Welcome dialog already acknowledged, skipping dialog display.", @@ -65,7 +65,7 @@ export class WelcomeModalDialogComponent { return; } - const dialog = dialogService.open(WelcomeModalDialogComponent, { + const dialog = dialogService.open(PostImportModalDialogComponent, { data: { organizationId } satisfies WelcomeModalDialogData, width: "600px", disableClose: true, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts index 193c732fcaa6..d5e3c4e44b40 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.spec.ts @@ -29,22 +29,22 @@ describe("OnboardingService", () => { service = TestBed.inject(OnboardingService); }); - describe("isCarouselAcknowledged", () => { + describe("isNewAdminWelcomeDialogAcknowledged", () => { it("returns false when state is null", async () => { mockStateProvider.getUserState$.mockReturnValue(of(null)); - const result = await service.isCarouselAcknowledged(); + const result = await service.isNewAdminWelcomeDialogAcknowledged(); expect(result).toBe(false); }); it("returns false when state is false", async () => { mockStateProvider.getUserState$.mockReturnValue(of(false)); - const result = await service.isCarouselAcknowledged(); + const result = await service.isNewAdminWelcomeDialogAcknowledged(); expect(result).toBe(false); }); it("returns true when state is true", async () => { mockStateProvider.getUserState$.mockReturnValue(of(true)); - const result = await service.isCarouselAcknowledged(); + const result = await service.isNewAdminWelcomeDialogAcknowledged(); expect(result).toBe(true); }); @@ -57,26 +57,26 @@ describe("OnboardingService", () => { { provide: StateProvider, useValue: mockStateProvider }, ], }); - const noAccountService = TestBed.inject(OnboardingService); - const result = await noAccountService.isCarouselAcknowledged(); + const service = TestBed.inject(OnboardingService); + const result = await service.isNewAdminWelcomeDialogAcknowledged(); expect(result).toBe(false); }); }); - describe("setCarouselAcknowledged", () => { + describe("setNewAdminWelcomeDialogAcknowledged", () => { it("calls setUserState with true by default", async () => { - await service.setCarouselAcknowledged(); + await service.setNewAdminWelcomeDialogAcknowledged(); expect(mockStateProvider.setUserState).toHaveBeenCalledWith( - expect.objectContaining({ key: "accessIntelligenceCarouselAcknowledged" }), + expect.objectContaining({ key: "accessIntelligenceNewAdminWelcomeAcknowledged" }), true, mockAccount.id, ); }); it("calls setUserState with the provided value", async () => { - await service.setCarouselAcknowledged(false); + await service.setNewAdminWelcomeDialogAcknowledged(false); expect(mockStateProvider.setUserState).toHaveBeenCalledWith( - expect.objectContaining({ key: "accessIntelligenceCarouselAcknowledged" }), + expect.objectContaining({ key: "accessIntelligenceNewAdminWelcomeAcknowledged" }), false, mockAccount.id, ); @@ -91,8 +91,8 @@ describe("OnboardingService", () => { { provide: StateProvider, useValue: mockStateProvider }, ], }); - const noAccountService = TestBed.inject(OnboardingService); - await noAccountService.setCarouselAcknowledged(); + const onboardingSvc = TestBed.inject(OnboardingService); + await onboardingSvc.setNewAdminWelcomeDialogAcknowledged(); expect(mockStateProvider.setUserState).not.toHaveBeenCalled(); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts index 7820008d5568..582fa8c67c32 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts @@ -8,21 +8,21 @@ import { UserKeyDefinition, } from "@bitwarden/state"; -const ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( +const ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( ACCESS_INTELLIGENCE_WELCOME_DIALOG_DISK, - "accessIntelligenceWelcomeDialogCompleted", + "accessIntelligencePostImportDialogCompleted", { deserializer: (value) => value, - clearOn: [], // Welcome dialog acknowledged state should persist across lock/logout so the dialog is not reshown + clearOn: [], // Post-import dialog acknowledged state should persist across lock/logout so the dialog is not reshown }, ); -const ACCESS_INTELLIGENCE_CAROUSEL_ACKNOWLEDGED_KEY = new UserKeyDefinition( +const ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY = new UserKeyDefinition( ACCESS_INTELLIGENCE_WELCOME_DIALOG_DISK, - "accessIntelligenceCarouselAcknowledged", + "accessIntelligenceNewAdminWelcomeAcknowledged", { deserializer: (value) => value, - clearOn: [], // Carousel acknowledged state should persist across lock/logout so the tour is not reshown + clearOn: [], // New admin welcome acknowledged state should persist across lock/logout so the tour is not reshown }, ); @@ -31,7 +31,7 @@ export class OnboardingService { private accountService = inject(AccountService); private stateProvider = inject(StateProvider); - async isWelcomeDialogAcknowledged(): Promise { + async isPostImportDialogAcknowledged(): Promise { const account = await firstValueFrom(this.accountService.activeAccount$); if (!account) { return false; @@ -39,25 +39,25 @@ export class OnboardingService { const acknowledged = await firstValueFrom( this.stateProvider - .getUserState$(ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY, account.id) + .getUserState$(ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY, account.id) .pipe(map((v) => v ?? false)), ); return acknowledged; } - async setWelcomeDialogAcknowledged(value = true) { + async setPostImportDialogAcknowledged(value = true) { const account = await firstValueFrom(this.accountService.activeAccount$); if (account) { await this.stateProvider.setUserState( - ACCESS_INTELLIGENCE_WELCOME_DIALOG_ACKNOWLEDGED_KEY, + ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY, value, account.id, ); } } - async isCarouselAcknowledged(): Promise { + async isNewAdminWelcomeDialogAcknowledged(): Promise { const account = await firstValueFrom(this.accountService.activeAccount$); if (!account) { return false; @@ -65,16 +65,16 @@ export class OnboardingService { return await firstValueFrom( this.stateProvider - .getUserState$(ACCESS_INTELLIGENCE_CAROUSEL_ACKNOWLEDGED_KEY, account.id) + .getUserState$(ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY, account.id) .pipe(map((v) => v ?? false)), ); } - async setCarouselAcknowledged(value = true) { + async setNewAdminWelcomeDialogAcknowledged(value = true) { const account = await firstValueFrom(this.accountService.activeAccount$); if (account) { await this.stateProvider.setUserState( - ACCESS_INTELLIGENCE_CAROUSEL_ACKNOWLEDGED_KEY, + ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY, value, account.id, ); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index db4073014f14..21c52ab85cac 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -60,8 +60,8 @@ import { ApplicationsComponent } from "./all-applications/applications.component import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component"; import { EmptyStateCardComponent } from "./empty-state-card.component"; import { RiskInsightsTabType } from "./models/risk-insights.models"; -import { WelcomeCarouselDialogComponent } from "./onboarding/welcome-carousel-dialog.component"; -import { WelcomeModalDialogComponent } from "./onboarding/welcome-modal-dialog.component"; +import { NewAdminWelcomeDialogComponent } from "./onboarding/new-admin-welcome-dialog.component"; +import { PostImportModalDialogComponent } from "./onboarding/post-import-modal-dialog.component"; import { DevMenuComponent } from "./shared/dev-menu.component"; import { PageLoadingComponent } from "./shared/page-loading.component"; import { ReportLoadingComponent } from "./shared/report-loading.component"; @@ -422,7 +422,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { protected async beginOnboardingTour(): Promise { if (this.adoptionUxImprovementsEnabled) { - await WelcomeModalDialogComponent.showWelcomeDialog( + await PostImportModalDialogComponent.showWelcomeDialog( this.injector, this.dialogService, this.organizationId, @@ -432,7 +432,11 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { protected async launchOnboardingWelcome(): Promise { if (this.adoptionUxImprovementsEnabled) { - await WelcomeCarouselDialogComponent.open(this.dialogService, this.organizationId); + await NewAdminWelcomeDialogComponent.showWelcomeCarouselDialog( + this.injector, + this.dialogService, + this.organizationId, + ); } } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts index ab3b73441d07..735d304c9d29 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts @@ -39,10 +39,11 @@ export class DevMenuComponent implements OnInit { protected readonly isOpen = signal(false); async ngOnInit(): Promise { - const isWelcomeDialogAcked = await this.onboardingService.isWelcomeDialogAcknowledged(); + const isWelcomeDialogAcked = await this.onboardingService.isPostImportDialogAcknowledged(); this.welcomeDialogAcked.set(isWelcomeDialogAcked); - const newAdminWelcomeDialogAcked = await this.onboardingService.isCarouselAcknowledged(); + const newAdminWelcomeDialogAcked = + await this.onboardingService.isNewAdminWelcomeDialogAcknowledged(); this.newAdminWelcomeDialogAcked.set(newAdminWelcomeDialogAcked); } @@ -85,7 +86,7 @@ export class DevMenuComponent implements OnInit { protected async onResetWelcomeDialogAck(): Promise { try { - await this.onboardingService.setWelcomeDialogAcknowledged(false); + await this.onboardingService.setPostImportDialogAcknowledged(false); this.welcomeDialogAcked.set(false); this.logger.info("Reset Access Intelligence welcome dialog acknowledged state."); } catch (error) { @@ -98,7 +99,7 @@ export class DevMenuComponent implements OnInit { protected async onShowWelcomeDialogAckState(): Promise { try { - const isAck = await this.onboardingService.isWelcomeDialogAcknowledged(); + const isAck = await this.onboardingService.isPostImportDialogAcknowledged(); this.welcomeDialogAcked.set(isAck); this.logger.info(`Access Intelligence welcome dialog acknowledged state: ${isAck}.`); } catch (error) { @@ -111,7 +112,7 @@ export class DevMenuComponent implements OnInit { protected async onResetNewAdminWelcomeDialogAck(): Promise { try { - await this.onboardingService.setCarouselAcknowledged(false); + await this.onboardingService.setNewAdminWelcomeDialogAcknowledged(false); this.newAdminWelcomeDialogAcked.set(false); this.logger.info("Reset Access Intelligence new admin welcome dialog acknowledged state."); } catch (error) { @@ -124,7 +125,7 @@ export class DevMenuComponent implements OnInit { protected async onShowNewAdminWelcomeDialogAckState(): Promise { try { - const isAck = await this.onboardingService.isCarouselAcknowledged(); + const isAck = await this.onboardingService.isNewAdminWelcomeDialogAcknowledged(); this.newAdminWelcomeDialogAcked.set(isAck); this.logger.info( `Access Intelligence new admin welcome dialog acknowledged state: ${isAck}.`, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts index 35bff71bb121..790d1aa515ad 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts @@ -47,7 +47,7 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { EmptyStateCardComponent } from "../../empty-state-card.component"; import { RiskInsightsTabType } from "../../models/risk-insights.models"; -import { WelcomeModalDialogComponent } from "../../onboarding/welcome-modal-dialog.component"; +import { PostImportModalDialogComponent } from "../../onboarding/post-import-modal-dialog.component"; import { DevMenuComponent } from "../../shared/dev-menu.component"; import { PageLoadingComponent } from "../../shared/page-loading.component"; import { ReportLoadingComponent } from "../../shared/report-loading.component"; @@ -448,7 +448,7 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { protected async beginOnboardingTour(): Promise { if (this.adoptionUxImprovementsEnabled()) { - await WelcomeModalDialogComponent.showWelcomeDialog( + await PostImportModalDialogComponent.showWelcomeDialog( this.injector, this.dialogService, this.organizationId(), From 0b1425b6b6e5087aa874d7bd8770cc35f6e8cc72 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Tue, 19 May 2026 13:32:16 -0500 Subject: [PATCH 10/18] PM-34724 refactored for consistency --- .../new-admin-welcome-dialog.component.ts | 4 +- .../post-import-modal-dialog.component.ts | 2 +- .../onboarding/services/onboarding.service.ts | 42 ++++++------------- .../risk-insights.component.html | 4 +- .../risk-insights.component.ts | 12 +++--- .../shared/dev-menu.component.html | 13 +++--- .../shared/dev-menu.component.ts | 28 ++++++------- .../access-intelligence-page.component.ts | 2 +- 8 files changed, 46 insertions(+), 61 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts index 2708e16fccba..1f17ac0ecfbf 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.ts @@ -64,7 +64,7 @@ export class NewAdminWelcomeDialogComponent { ); } - static async showWelcomeCarouselDialog( + static async showDialog( injector: Injector, dialogService: DialogService, organizationId: OrganizationId, @@ -75,7 +75,7 @@ export class NewAdminWelcomeDialogComponent { const acknowledged = await onboardingService.isNewAdminWelcomeDialogAcknowledged(); if (acknowledged) { logger.info( - "[Access Intelligence Onboarding] Welcome carousel already acknowledged, skipping dialog display.", + "[Access Intelligence Onboarding] Welcome dialog already acknowledged, skipping dialog display.", ); return; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts index 446f1bf157f1..919f4c14fd21 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/post-import-modal-dialog.component.ts @@ -49,7 +49,7 @@ export class PostImportModalDialogComponent { .catch(() => {}); } - static async showWelcomeDialog( + static async showDialog( injector: Injector, dialogService: DialogService, organizationId: OrganizationId, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts index 582fa8c67c32..51fc499dd553 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/services/onboarding.service.ts @@ -32,52 +32,36 @@ export class OnboardingService { private stateProvider = inject(StateProvider); async isPostImportDialogAcknowledged(): Promise { - const account = await firstValueFrom(this.accountService.activeAccount$); - if (!account) { - return false; - } - - const acknowledged = await firstValueFrom( - this.stateProvider - .getUserState$(ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY, account.id) - .pipe(map((v) => v ?? false)), - ); - - return acknowledged; + return this.isAcknowledged(ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY); } async setPostImportDialogAcknowledged(value = true) { - const account = await firstValueFrom(this.accountService.activeAccount$); - if (account) { - await this.stateProvider.setUserState( - ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY, - value, - account.id, - ); - } + await this.setAcknowledged(ACCESS_INTELLIGENCE_POST_IMPORT_DIALOG_ACKNOWLEDGED_KEY, value); } async isNewAdminWelcomeDialogAcknowledged(): Promise { + return this.isAcknowledged(ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY); + } + + async setNewAdminWelcomeDialogAcknowledged(value = true) { + await this.setAcknowledged(ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY, value); + } + + private async isAcknowledged(key: UserKeyDefinition): Promise { const account = await firstValueFrom(this.accountService.activeAccount$); if (!account) { return false; } return await firstValueFrom( - this.stateProvider - .getUserState$(ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY, account.id) - .pipe(map((v) => v ?? false)), + this.stateProvider.getUserState$(key, account.id).pipe(map((v) => v ?? false)), ); } - async setNewAdminWelcomeDialogAcknowledged(value = true) { + private async setAcknowledged(key: UserKeyDefinition, value = true) { const account = await firstValueFrom(this.accountService.activeAccount$); if (account) { - await this.stateProvider.setUserState( - ACCESS_INTELLIGENCE_NEW_ADMIN_WELCOME_ACKNOWLEDGED_KEY, - value, - account.id, - ); + await this.stateProvider.setUserState(key, value, account.id); } } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index e8a2b4e8109b..45e4a868808b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -109,8 +109,8 @@ @if (isDevMode || adoptionUxImprovementsEnabled) { } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index 21c52ab85cac..fab31dd20acc 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -286,7 +286,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { take(1), ) .subscribe(() => { - void this.launchOnboardingWelcome().catch((error: unknown) => { + void this.beginNewAdminWelcomeTour().catch((error: unknown) => { this.logService.error("Failed to launch onboarding welcome", error); }); }); @@ -404,7 +404,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { ): Promise { if (source === "import" && status === "success") { this.generateReport(); - await this.beginOnboardingTour(); + await this.beginPostImportTour(); } this.clearQueryParams(this.router, this.route, ["source", "status"]); @@ -420,9 +420,9 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { }); } - protected async beginOnboardingTour(): Promise { + protected async beginPostImportTour(): Promise { if (this.adoptionUxImprovementsEnabled) { - await PostImportModalDialogComponent.showWelcomeDialog( + await PostImportModalDialogComponent.showDialog( this.injector, this.dialogService, this.organizationId, @@ -430,9 +430,9 @@ export class RiskInsightsComponent implements OnInit, OnDestroy { } } - protected async launchOnboardingWelcome(): Promise { + protected async beginNewAdminWelcomeTour(): Promise { if (this.adoptionUxImprovementsEnabled) { - await NewAdminWelcomeDialogComponent.showWelcomeCarouselDialog( + await NewAdminWelcomeDialogComponent.showDialog( this.injector, this.dialogService, this.organizationId, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html index 695f81c694d5..9f9cbddb15b8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.html @@ -16,6 +16,7 @@ Import Data from Access Intelligence
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts index 735d304c9d29..5d5a83fd5928 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/dev-menu.component.ts @@ -30,17 +30,17 @@ export class DevMenuComponent implements OnInit { private readonly elementRef = inject(ElementRef); private readonly onboardingService = inject(OnboardingService); private readonly logger = inject(LogService); - protected readonly welcomeDialogAcked = signal(false); + protected readonly postImportDialogAcked = signal(false); protected readonly newAdminWelcomeDialogAcked = signal(false); readonly beginNewAdminWelcomeTour = output(); - readonly beginTour = output(); + readonly beginPostImportTour = output(); readonly importData = output(); protected readonly isOpen = signal(false); async ngOnInit(): Promise { - const isWelcomeDialogAcked = await this.onboardingService.isPostImportDialogAcknowledged(); - this.welcomeDialogAcked.set(isWelcomeDialogAcked); + const isPostImportDialogAcked = await this.onboardingService.isPostImportDialogAcknowledged(); + this.postImportDialogAcked.set(isPostImportDialogAcked); const newAdminWelcomeDialogAcked = await this.onboardingService.isNewAdminWelcomeDialogAcknowledged(); @@ -69,9 +69,9 @@ export class DevMenuComponent implements OnInit { } } - protected onBeginTour(): void { + protected onBeginPostImportTour(): void { this.isOpen.set(false); - this.beginTour.emit(); + this.beginPostImportTour.emit(); } protected onBeginNewAdminWelcomeTour(): void { @@ -84,27 +84,27 @@ export class DevMenuComponent implements OnInit { this.importData.emit(); } - protected async onResetWelcomeDialogAck(): Promise { + protected async onResetPostImportDialogAck(): Promise { try { await this.onboardingService.setPostImportDialogAcknowledged(false); - this.welcomeDialogAcked.set(false); - this.logger.info("Reset Access Intelligence welcome dialog acknowledged state."); + this.postImportDialogAcked.set(false); + this.logger.info("Reset Access Intelligence post import dialog acknowledged state."); } catch (error) { this.logger.error( - "Failed to reset Access Intelligence welcome dialog acknowledged state.", + "Failed to reset Access Intelligence post import dialog acknowledged state.", error, ); } } - protected async onShowWelcomeDialogAckState(): Promise { + protected async onShowPostImportDialogAckState(): Promise { try { const isAck = await this.onboardingService.isPostImportDialogAcknowledged(); - this.welcomeDialogAcked.set(isAck); - this.logger.info(`Access Intelligence welcome dialog acknowledged state: ${isAck}.`); + this.postImportDialogAcked.set(isAck); + this.logger.info(`Access Intelligence post import dialog acknowledged state: ${isAck}.`); } catch (error) { this.logger.error( - "Failed to get Access Intelligence welcome dialog acknowledged state.", + "Failed to get Access Intelligence post import dialog acknowledged state.", error, ); } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts index 790d1aa515ad..63ae6d5354e6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/v2/access-intelligence-page/access-intelligence-page.component.ts @@ -448,7 +448,7 @@ export class AccessIntelligencePageComponent implements OnInit, OnDestroy { protected async beginOnboardingTour(): Promise { if (this.adoptionUxImprovementsEnabled()) { - await PostImportModalDialogComponent.showWelcomeDialog( + await PostImportModalDialogComponent.showDialog( this.injector, this.dialogService, this.organizationId(), From d2d0fc34a18257f10b7dc75d9224dd797778a12c Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Tue, 19 May 2026 13:57:04 -0500 Subject: [PATCH 11/18] PM-34724 fix build in stories --- .../new-admin-welcome-dialog.component.stories.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts index 8ddb89fe9a51..8204dc5162c9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts @@ -12,16 +12,16 @@ import { } from "@bitwarden/components"; import { VaultCarouselModule } from "@bitwarden/vault"; +import { NewAdminWelcomeDialogComponent } from "./new-admin-welcome-dialog.component"; import { OnboardingService } from "./services/onboarding.service"; -import { WelcomeCarouselDialogComponent } from "./welcome-carousel-dialog.component"; const mockDialogRef = { close: async () => {} }; const mockOnboardingService = { setCarouselAcknowledged: async () => {} }; const mockOrganizationId = "story-org-id" as OrganizationId; export default { - title: "Access Intelligence/WelcomeCarouselDialog", - component: WelcomeCarouselDialogComponent, + title: "Access Intelligence/NewAdminWelcomeDialog", + component: NewAdminWelcomeDialogComponent, decorators: [ moduleMetadata({ imports: [VaultCarouselModule, DialogModule, ButtonModule, TypographyModule], @@ -36,6 +36,6 @@ export default { ], } as Meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; From 8f7538a2de471645aea8d2a9cd94b64ac7b4d19c Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Tue, 19 May 2026 14:19:48 -0500 Subject: [PATCH 12/18] PM-34724 updated i18n text for story --- ...-admin-welcome-dialog.component.stories.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts index 8204dc5162c9..184ca2c3b7c8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts @@ -9,6 +9,7 @@ import { DialogRef, DIALOG_DATA, TypographyModule, + I18nMockService, } from "@bitwarden/components"; import { VaultCarouselModule } from "@bitwarden/vault"; @@ -19,6 +20,28 @@ const mockDialogRef = { close: async () => {} }; const mockOnboardingService = { setCarouselAcknowledged: async () => {} }; const mockOrganizationId = "story-org-id" as OrganizationId; +const mockI18nService = new I18nMockService({ + accessIntelligenceWelcomeTour: "Welcome to Access Intelligence!", + yourEntireOrgsSecurityInOneView: "Your entire org's security in one view", + accessIntelligenceGivesYouSinglePlace: + "Access Intelligence gives you a single place to view and manage your organization's security posture, so you can spend less time on security administration and more time on strategic initiatives.", + youSetThePrioritiesWeSurfaceTheRisks: + "You set the priorities, we surface the risks. With Access Intelligence, you can easily identify and remediate security risks across your organization, all while empowering your end users to take charge of their own security.", + youMarkWhichAppsAreMostCritical: + "You mark which apps are most critical to your org, and Access Intelligence surfaces the riskiest accounts and weakest links in those apps, so you can focus on what matters most.", + trackImprovementsAcrossYourTeam: + "Track improvements across your team and show off your wins to leadership with Access Intelligence's reporting and analytics features.", + membersAreAutomaticallyNotified: + "Members are automatically notified of security risks and can take action to resolve them, making it easier than ever to maintain a strong security posture across your organization.", + importYourOrgDataToGetStarted: "Import your org data to get started", + onceItHasTheVaultData: + "Once it has the vault data, Access Intelligence can start surfacing insights and recommendations to help you improve your organization's security.", + skip: "Skip", + back: "Back", + next: "Next", + importData: "Import Data", +}); + export default { title: "Access Intelligence/NewAdminWelcomeDialog", component: NewAdminWelcomeDialogComponent, @@ -26,7 +49,7 @@ export default { moduleMetadata({ imports: [VaultCarouselModule, DialogModule, ButtonModule, TypographyModule], providers: [ - { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: I18nService, useValue: mockI18nService }, { provide: DialogRef, useValue: mockDialogRef }, { provide: OnboardingService, useValue: mockOnboardingService }, { provide: DIALOG_DATA, useValue: { organizationId: mockOrganizationId } }, From f94871e3f482f47242622cde71346481f5430162 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Wed, 20 May 2026 13:13:09 -0500 Subject: [PATCH 13/18] PM-34724 updated carousel to use signal and modified text for story --- .../new-admin-welcome-dialog.component.stories.ts | 6 ++---- libs/vault/src/components/carousel/carousel.component.html | 4 ++-- libs/vault/src/components/carousel/carousel.component.ts | 6 ++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts index 184ca2c3b7c8..fb4f9c0e9cbd 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/onboarding/new-admin-welcome-dialog.component.stories.ts @@ -25,12 +25,10 @@ const mockI18nService = new I18nMockService({ yourEntireOrgsSecurityInOneView: "Your entire org's security in one view", accessIntelligenceGivesYouSinglePlace: "Access Intelligence gives you a single place to view and manage your organization's security posture, so you can spend less time on security administration and more time on strategic initiatives.", - youSetThePrioritiesWeSurfaceTheRisks: - "You set the priorities, we surface the risks. With Access Intelligence, you can easily identify and remediate security risks across your organization, all while empowering your end users to take charge of their own security.", + youSetThePrioritiesWeSurfaceTheRisks: "You set the priorities, we surface the risks.", youMarkWhichAppsAreMostCritical: "You mark which apps are most critical to your org, and Access Intelligence surfaces the riskiest accounts and weakest links in those apps, so you can focus on what matters most.", - trackImprovementsAcrossYourTeam: - "Track improvements across your team and show off your wins to leadership with Access Intelligence's reporting and analytics features.", + trackImprovementsAcrossYourTeam: "Track improvements across your team.", membersAreAutomaticallyNotified: "Members are automatically notified of security risks and can take action to resolve them, making it easier than ever to maintain a strong security posture across your organization.", importYourOrgDataToGetStarted: "Import your org data to get started", diff --git a/libs/vault/src/components/carousel/carousel.component.html b/libs/vault/src/components/carousel/carousel.component.html index 5db7945a6d73..25a82e6a2c85 100644 --- a/libs/vault/src/components/carousel/carousel.component.html +++ b/libs/vault/src/components/carousel/carousel.component.html @@ -8,7 +8,7 @@
- @if (!hideArrows) { + @if (!hideArrows()) {