1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-11 10:10:25 +01:00

[PM-10653] MP Reprompt Check for Inline Autofill (#10546)

* add logic for MP reprompt on autofill of web input opening popout browser
This commit is contained in:
Jason Ng 2024-08-22 16:38:21 -04:00 committed by GitHub
parent 83ed0442de
commit 404d514e53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 72 additions and 15 deletions

View File

@ -79,7 +79,7 @@ export class ItemMoreOptionsComponent {
} }
async doAutofillAndSave() { async doAutofillAndSave() {
await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher); await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, false);
} }
/** /**

View File

@ -16,6 +16,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
import { ViewV2Component } from "./view-v2.component"; import { ViewV2Component } from "./view-v2.component";
// 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile. // 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile.
@ -34,6 +35,9 @@ describe("ViewV2Component", () => {
type: CipherType.Login, type: CipherType.Login,
}; };
const mockVaultPopupAutofillService = {
doAutofill: jest.fn(),
};
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@ -66,6 +70,7 @@ describe("ViewV2Component", () => {
}, },
}, },
}, },
{ provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService },
{ {
provide: AccountService, provide: AccountService,
useValue: accountService, useValue: accountService,

View File

@ -8,6 +8,7 @@ import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AUTOFILL_ID, SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -32,6 +33,7 @@ import { BrowserTotpCaptureService } from "../../../services/browser-totp-captur
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component";
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
@Component({ @Component({
selector: "app-view-v2", selector: "app-view-v2",
@ -59,6 +61,7 @@ export class ViewV2Component {
organization$: Observable<Organization>; organization$: Observable<Organization>;
folder$: Observable<FolderView>; folder$: Observable<FolderView>;
collections$: Observable<CollectionView[]>; collections$: Observable<CollectionView[]>;
loadAction: typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -68,6 +71,7 @@ export class ViewV2Component {
private dialogService: DialogService, private dialogService: DialogService,
private logService: LogService, private logService: LogService,
private toastService: ToastService, private toastService: ToastService,
private vaultPopupAutofillService: VaultPopupAutofillService,
private accountService: AccountService, private accountService: AccountService,
) { ) {
this.subscribeToParams(); this.subscribeToParams();
@ -77,14 +81,19 @@ export class ViewV2Component {
this.route.queryParams this.route.queryParams
.pipe( .pipe(
switchMap(async (params): Promise<CipherView> => { switchMap(async (params): Promise<CipherView> => {
this.loadAction = params.action;
return await this.getCipherData(params.cipherId); return await this.getCipherData(params.cipherId);
}), }),
switchMap(async (cipher) => {
this.cipher = cipher;
this.headerText = this.setHeader(cipher.type);
if (this.loadAction === AUTOFILL_ID || this.loadAction === SHOW_AUTOFILL_BUTTON) {
await this.vaultPopupAutofillService.doAutofill(this.cipher);
}
}),
takeUntilDestroyed(), takeUntilDestroyed(),
) )
.subscribe((cipher) => { .subscribe();
this.cipher = cipher;
this.headerText = this.setHeader(cipher.type);
});
} }
setHeader(type: CipherType) { setHeader(type: CipherType) {

View File

@ -1,6 +1,7 @@
import { TestBed } from "@angular/core/testing"; import { TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject, of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -33,6 +34,9 @@ describe("VaultPopupAutofillService", () => {
let service: VaultPopupAutofillService; let service: VaultPopupAutofillService;
const mockCurrentTab = { url: "https://example.com" } as chrome.tabs.Tab; const mockCurrentTab = { url: "https://example.com" } as chrome.tabs.Tab;
const mockActivatedRoute = {
queryParams: of({}),
} as any;
// Create mocks for VaultPopupAutofillService // Create mocks for VaultPopupAutofillService
const mockAutofillService = mock<AutofillService>(); const mockAutofillService = mock<AutofillService>();
@ -61,6 +65,7 @@ describe("VaultPopupAutofillService", () => {
{ provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, { provide: PasswordRepromptService, useValue: mockPasswordRepromptService },
{ provide: CipherService, useValue: mockCipherService }, { provide: CipherService, useValue: mockCipherService },
{ provide: MessagingService, useValue: mockMessagingService }, { provide: MessagingService, useValue: mockMessagingService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ {
provide: AccountService, provide: AccountService,
useValue: accountService, useValue: accountService,
@ -258,6 +263,17 @@ describe("VaultPopupAutofillService", () => {
expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenCalledTimes(1);
expect(BrowserApi.closePopup).toHaveBeenCalled(); expect(BrowserApi.closePopup).toHaveBeenCalled();
}); });
it("should show a successful toast message if login form is populated", async () => {
jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValue(true);
(service as any).currentAutofillTab$ = of({ id: 1234 });
await service.doAutofill(mockCipher);
expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "success",
title: null,
message: mockI18nService.t("autoFillSuccess"),
});
});
}); });
}); });

View File

@ -1,5 +1,7 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { import {
combineLatest,
firstValueFrom, firstValueFrom,
map, map,
Observable, Observable,
@ -27,20 +29,30 @@ import {
} from "../../../autofill/services/abstractions/autofill.service"; } from "../../../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { closeViewVaultItemPopout, VaultPopoutType } from "../utils/vault-popout-window";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
export class VaultPopupAutofillService { export class VaultPopupAutofillService {
private _refreshCurrentTab$ = new Subject<void>(); private _refreshCurrentTab$ = new Subject<void>();
private senderTabId$: Observable<number | undefined> = this.route.queryParams.pipe(
map((params) => (params?.senderTabId ? parseInt(params.senderTabId, 10) : undefined)),
);
/** /**
* Observable that contains the current tab to be considered for autofill. If there is no current tab * Observable that contains the current tab to be considered for autofill.
* or the popup is in a popout window, this will be null. * This can be the tab from the current window if opened in a Popup OR
* the sending tab when opened the single action Popout (specified by the senderTabId route query parameter)
*/ */
currentAutofillTab$: Observable<chrome.tabs.Tab | null> = this._refreshCurrentTab$.pipe( currentAutofillTab$: Observable<chrome.tabs.Tab | null> = combineLatest([
startWith(null), this.senderTabId$,
switchMap(async () => { this._refreshCurrentTab$.pipe(startWith(null)),
]).pipe(
switchMap(async ([senderTabId]) => {
if (senderTabId) {
return await BrowserApi.getTab(senderTabId);
}
if (BrowserPopupUtils.inPopout(window)) { if (BrowserPopupUtils.inPopout(window)) {
return null; return null;
} }
@ -73,6 +85,7 @@ export class VaultPopupAutofillService {
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private cipherService: CipherService, private cipherService: CipherService,
private messagingService: MessagingService, private messagingService: MessagingService,
private route: ActivatedRoute,
private accountService: AccountService, private accountService: AccountService,
) { ) {
this._currentPageDetails$.subscribe(); this._currentPageDetails$.subscribe();
@ -124,7 +137,21 @@ export class VaultPopupAutofillService {
return true; return true;
} }
private _closePopup() { private async _closePopup(cipher: CipherView, tab: chrome.tabs.Tab | null) {
if (BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) && tab.id) {
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("autoFillSuccess"),
});
setTimeout(async () => {
await BrowserApi.focusTab(tab.id);
await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${cipher.id}`);
}, 1000);
return;
}
if (!BrowserPopupUtils.inPopup(window)) { if (!BrowserPopupUtils.inPopup(window)) {
return; return;
} }
@ -158,7 +185,7 @@ export class VaultPopupAutofillService {
const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails); const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails);
if (didAutofill && closePopup) { if (didAutofill && closePopup) {
this._closePopup(); await this._closePopup(cipher, tab);
} }
return didAutofill; return didAutofill;
@ -193,7 +220,7 @@ export class VaultPopupAutofillService {
} }
if (closePopup) { if (closePopup) {
this._closePopup(); await this._closePopup(cipher, tab);
} else { } else {
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",