From 392794b560cdc598c322a99930eff0dc50a9253f Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 9 Jan 2026 12:41:39 -0500 Subject: [PATCH] Ac/pm 26365 auto confirm extension one time setup dialog (#17339) * create nav link for auto confirm in settings page * wip * WIP * create auto confirm library * migrate auto confirm files to lib * update imports * fix tests * fix nudge * cleanup, add documentation * clean up * cleanup * fix import * fix more imports * implement one time dialog * add tests * design changes * fix styles * edit copy * fix tests * fix tw issue * fix typo, add tests * CR feedback * more clean up, fix race condition * CR feedback, cache policies, refactor tests * run prettier with updated version * clean up duplicate logic * clean up * add missing export * fix test * fix dialog position * add tests --- apps/browser/src/_locales/en/messages.json | 15 +++ .../vault-v2/vault-v2.component.spec.ts | 103 ++++++++++++++++++ .../components/vault-v2/vault-v2.component.ts | 39 +++++++ ...auto-confirm-extension-dialog.component.ts | 78 +++++++++++++ .../auto-confirm-warning-dialog.component.ts | 11 +- libs/auto-confirm/src/components/index.ts | 1 + 6 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 29b39863bc6..1613373bd62 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4829,6 +4829,21 @@ "autoConfirmWarningLink": { "message": "Learn about the risks" }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts index 883d17b61c3..e6dffdaff08 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts @@ -10,6 +10,10 @@ import { BehaviorSubject, Observable, Subject, of } from "rxjs"; import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components"; import { NudgeType, NudgesService } from "@bitwarden/angular/vault"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { + AutoConfirmExtensionSetupDialogComponent, + AutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { CurrentAccountComponent } from "@bitwarden/browser/auth/popup/account-switching/current-account.component"; import AutofillService from "@bitwarden/browser/autofill/services/autofill.service"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; @@ -136,6 +140,7 @@ class VaultListItemsContainerStubComponent { const mockDialogRef = { close: jest.fn(), afterClosed: jest.fn().mockReturnValue(of(undefined)), + closed: of(undefined), } as unknown as import("@bitwarden/components").DialogRef; jest @@ -145,6 +150,11 @@ jest jest .spyOn(DecryptionFailureDialogComponent, "open") .mockImplementation((_: DialogService, _params: any) => mockDialogRef as any); + +const autoConfirmDialogSpy = jest + .spyOn(AutoConfirmExtensionSetupDialogComponent, "open") + .mockImplementation((_: DialogService) => mockDialogRef as any); + jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(false); jest.spyOn(BrowserPopupUtils, "openCurrentPagePopout").mockResolvedValue(); @@ -222,6 +232,13 @@ describe("VaultV2Component", () => { getFeatureFlag$: jest.fn().mockImplementation((_flag: string) => of(false)), }; + const autoConfirmSvc = { + configuration$: jest.fn().mockReturnValue(of({})), + canManageAutoConfirm$: jest.fn().mockReturnValue(of(false)), + upsert: jest.fn().mockResolvedValue(undefined), + autoConfirmUser: jest.fn().mockResolvedValue(undefined), + }; + beforeEach(async () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ @@ -275,6 +292,10 @@ describe("VaultV2Component", () => { provide: SearchService, useValue: { isCipherSearching$: of(false) }, }, + { + provide: AutomaticUserConfirmationService, + useValue: autoConfirmSvc, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); @@ -588,4 +609,86 @@ describe("VaultV2Component", () => { const spotlights = queryAllSpotlights(fixture); expect(spotlights.length).toBe(0); })); + + describe("AutoConfirmExtensionSetupDialog", () => { + beforeEach(() => { + autoConfirmDialogSpy.mockClear(); + }); + + it("opens dialog when canManage is true and showBrowserNotification is undefined", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(true)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: false, + showSetupDialog: true, + showBrowserNotification: undefined, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).toHaveBeenCalledWith(expect.any(Object)); + })); + + it("does not open dialog when showBrowserNotification is false", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(true)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: false, + showSetupDialog: true, + showBrowserNotification: false, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).not.toHaveBeenCalled(); + })); + + it("does not open dialog when showBrowserNotification is true", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(true)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: true, + showSetupDialog: true, + showBrowserNotification: true, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).not.toHaveBeenCalled(); + })); + + it("does not open dialog when canManage is false even if showBrowserNotification is undefined", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(false)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: false, + showSetupDialog: true, + showBrowserNotification: undefined, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).not.toHaveBeenCalled(); + })); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 30d1d21abfb..761b366bcd2 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -15,6 +15,7 @@ import { shareReplay, switchMap, take, + withLatestFrom, tap, BehaviorSubject, } from "rxjs"; @@ -25,6 +26,11 @@ import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { DeactivatedOrg, NoResults, VaultOpen } from "@bitwarden/assets/svg"; +import { + AutoConfirmExtensionSetupDialogComponent, + AutoConfirmState, + AutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; @@ -41,6 +47,7 @@ import { ButtonModule, DialogService, NoItemsModule, + ToastService, TypographyModule, } from "@bitwarden/components"; import { @@ -267,6 +274,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { private introCarouselService: IntroCarouselService, private nudgesService: NudgesService, private router: Router, + private autoConfirmService: AutomaticUserConfirmationService, + private toastService: ToastService, private vaultProfileService: VaultProfileService, private billingAccountService: BillingAccountProfileStateService, private liveAnnouncer: LiveAnnouncer, @@ -329,6 +338,36 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { }); }); + const autoConfirmState$ = this.autoConfirmService.configuration$(this.activeUserId); + + combineLatest([ + this.autoConfirmService.canManageAutoConfirm$(this.activeUserId), + autoConfirmState$, + ]) + .pipe( + filter(([canManage, state]) => canManage && state.showBrowserNotification === undefined), + take(1), + switchMap(() => AutoConfirmExtensionSetupDialogComponent.open(this.dialogService).closed), + withLatestFrom(autoConfirmState$, this.accountService.activeAccount$.pipe(getUserId)), + switchMap(([result, state, userId]) => { + const newState: AutoConfirmState = { + ...state, + enabled: result ?? false, + showBrowserNotification: !result, + }; + + if (result) { + this.toastService.showToast({ + message: this.i18nService.t("autoConfirmEnabled"), + variant: "success", + }); + } + + return this.autoConfirmService.upsert(userId, newState); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); await this.vaultItemsTransferService.enforceOrganizationDataOwnership(this.activeUserId); this.readySubject.next(true); diff --git a/libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts b/libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts new file mode 100644 index 00000000000..c04d8b5209b --- /dev/null +++ b/libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts @@ -0,0 +1,78 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + BadgeComponent, + ButtonModule, + CenterPositionStrategy, + DialogModule, + DialogService, +} from "@bitwarden/components"; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
+
+ {{ "availableNow" | i18n }} +
+
+

+ + {{ "autoConfirmSetup" | i18n }} + +

+ + {{ "autoConfirmSetupDesc" | i18n }} + +
+
+ +
+ + + + + {{ "autoConfirmSetupHint" | i18n }} + + + +
+
+
+ `, + imports: [ButtonModule, DialogModule, CommonModule, JslibModule, BadgeComponent], +}) +export class AutoConfirmExtensionSetupDialogComponent { + constructor(public dialogRef: DialogRef) {} + + static open(dialogService: DialogService) { + return dialogService.open(AutoConfirmExtensionSetupDialogComponent, { + positionStrategy: new CenterPositionStrategy(), + }); + } +} diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts b/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts index f126ce3b92c..877a0fe918a 100644 --- a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts +++ b/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts @@ -2,7 +2,12 @@ import { DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component } from "@angular/core"; -import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { + ButtonModule, + CenterPositionStrategy, + DialogModule, + DialogService, +} from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @Component({ @@ -14,6 +19,8 @@ export class AutoConfirmWarningDialogComponent { constructor(public dialogRef: DialogRef) {} static open(dialogService: DialogService) { - return dialogService.open(AutoConfirmWarningDialogComponent); + return dialogService.open(AutoConfirmWarningDialogComponent, { + positionStrategy: new CenterPositionStrategy(), + }); } } diff --git a/libs/auto-confirm/src/components/index.ts b/libs/auto-confirm/src/components/index.ts index a0310e805c6..1cddd1d7e59 100644 --- a/libs/auto-confirm/src/components/index.ts +++ b/libs/auto-confirm/src/components/index.ts @@ -1 +1,2 @@ +export * from "./auto-confirm-extension-dialog.component"; export * from "./auto-confirm-warning-dialog.component";