1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-21 11:53:34 +00:00

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
This commit is contained in:
Brandon Treston
2026-01-09 12:41:39 -05:00
committed by GitHub
parent c6f704bd21
commit 392794b560
6 changed files with 245 additions and 2 deletions

View File

@@ -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"
},

View File

@@ -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<any, any>;
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();
}));
});
});

View File

@@ -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);

View File

@@ -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: `
<bit-simple-dialog dialogSize="small" hideIcon>
<div class="tw-flex tw-flex-col tw-justify-start" bitDialogTitle>
<div class="tw-flex tw-justify-start tw-pb-2">
<span bitBadge variant="info"> {{ "availableNow" | i18n }}</span>
</div>
<div class="tw-flex tw-flex-col">
<h3 class="tw-text-start">
<strong>
{{ "autoConfirmSetup" | i18n }}
</strong>
</h3>
<span class="tw-overflow-y-auto tw-text-start tw-break-words tw-hyphens-auto tw-text-sm">
{{ "autoConfirmSetupDesc" | i18n }}
</span>
</div>
</div>
<ng-container bitDialogFooter>
<div class="tw-flex tw-flex-col tw-justify-center">
<button
class="tw-mb-2"
type="button"
bitButton
buttonType="primary"
(click)="dialogRef.close(true)"
>
{{ "turnOn" | i18n }}
</button>
<button
class="tw-mb-4"
type="button"
bitButton
buttonType="secondary"
(click)="dialogRef.close(false)"
>
{{ "close" | i18n }}
</button>
<a
class="tw-text-sm tw-text-center"
bitLink
href="https://bitwarden.com/help/automatic-confirmation/"
target="_blank"
>
<strong class="tw-pr-1">
{{ "autoConfirmSetupHint" | i18n }}
</strong>
<i class="bwi bwi-external-link bwi-fw"></i>
</a>
</div>
</ng-container>
</bit-simple-dialog>
`,
imports: [ButtonModule, DialogModule, CommonModule, JslibModule, BadgeComponent],
})
export class AutoConfirmExtensionSetupDialogComponent {
constructor(public dialogRef: DialogRef<boolean>) {}
static open(dialogService: DialogService) {
return dialogService.open<boolean>(AutoConfirmExtensionSetupDialogComponent, {
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -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<boolean>) {}
static open(dialogService: DialogService) {
return dialogService.open<boolean>(AutoConfirmWarningDialogComponent);
return dialogService.open<boolean>(AutoConfirmWarningDialogComponent, {
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -1 +1,2 @@
export * from "./auto-confirm-extension-dialog.component";
export * from "./auto-confirm-warning-dialog.component";