mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-09 09:51:02 +01:00
[PM-8486] Browser Refresh - Autofill functionality (#9616)
* [PM-8486] Introduce VaultPopupAutofill service * [PM-8486] Remove moved autofill functionality from VaultPopupItem service * [PM-8486] Add autofill functionality to button and menu options * [PM-8486] Hide Autofill and Save option for Cards/Identities * [PM-8486] Reduce nesting in closePopup * [PM-8486] Breakup doAutofillAndSave method * [PM-8486] Start subscription in autofill service constructor * [PM-8486] Cleanup missed calls to removed methods
This commit is contained in:
parent
2042b3a26c
commit
b7a961bf1f
@ -12,6 +12,7 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||||
|
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||||
import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component";
|
import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component";
|
||||||
@ -53,7 +54,7 @@ export class AutofillVaultListItemsComponent {
|
|||||||
protected showEmptyAutofillTip$: Observable<boolean> = combineLatest([
|
protected showEmptyAutofillTip$: Observable<boolean> = combineLatest([
|
||||||
this.vaultPopupItemsService.hasFilterApplied$,
|
this.vaultPopupItemsService.hasFilterApplied$,
|
||||||
this.autofillCiphers$,
|
this.autofillCiphers$,
|
||||||
this.vaultPopupItemsService.autofillAllowed$,
|
this.vaultPopupAutofillService.autofillAllowed$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(
|
map(
|
||||||
([hasFilter, ciphers, canAutoFill]) =>
|
([hasFilter, ciphers, canAutoFill]) =>
|
||||||
@ -61,7 +62,10 @@ export class AutofillVaultListItemsComponent {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
|
constructor(
|
||||||
|
private vaultPopupItemsService: VaultPopupItemsService,
|
||||||
|
private vaultPopupAutofillService: VaultPopupAutofillService,
|
||||||
|
) {
|
||||||
// TODO: Migrate logic to show Autofill policy toast PM-8144
|
// TODO: Migrate logic to show Autofill policy toast PM-8144
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +74,6 @@ export class AutofillVaultListItemsComponent {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected refreshCurrentTab() {
|
protected refreshCurrentTab() {
|
||||||
this.vaultPopupItemsService.refreshCurrentTab();
|
this.vaultPopupAutofillService.refreshCurrentTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@
|
|||||||
<bit-menu #moreOptions>
|
<bit-menu #moreOptions>
|
||||||
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
|
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
|
||||||
<ng-container *ngIf="autofillAllowed$ | async">
|
<ng-container *ngIf="autofillAllowed$ | async">
|
||||||
<button type="button" bitMenuItem>
|
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||||
{{ "autofill" | i18n }}
|
{{ "autofill" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem *ngIf="canEdit">
|
<button type="button" bitMenuItem *ngIf="canEdit && isLogin" (click)="doAutofillAndSave()">
|
||||||
{{ "fillAndSave" | i18n }}
|
{{ "fillAndSave" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -18,7 +18,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
|||||||
|
|
||||||
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 { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -39,16 +39,16 @@ export class ItemMoreOptionsComponent {
|
|||||||
@Input({ transform: booleanAttribute })
|
@Input({ transform: booleanAttribute })
|
||||||
hideAutofillOptions: boolean;
|
hideAutofillOptions: boolean;
|
||||||
|
|
||||||
protected autofillAllowed$ = this.vaultPopupItemsService.autofillAllowed$;
|
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private vaultPopupItemsService: VaultPopupItemsService,
|
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private vaultPopupAutofillService: VaultPopupAutofillService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get canEdit() {
|
get canEdit() {
|
||||||
@ -62,10 +62,22 @@ export class ItemMoreOptionsComponent {
|
|||||||
return [CipherType.Login, CipherType.Card, CipherType.Identity].includes(this.cipher.type);
|
return [CipherType.Login, CipherType.Card, CipherType.Identity].includes(this.cipher.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isLogin() {
|
||||||
|
return this.cipher.type === CipherType.Login;
|
||||||
|
}
|
||||||
|
|
||||||
get favoriteText() {
|
get favoriteText() {
|
||||||
return this.cipher.favorite ? "unfavorite" : "favorite";
|
return this.cipher.favorite ? "unfavorite" : "favorite";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doAutofill() {
|
||||||
|
await this.vaultPopupAutofillService.doAutofill(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
async doAutofillAndSave() {
|
||||||
|
await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the login cipher can be launched in a new browser tab.
|
* Determines if the login cipher can be launched in a new browser tab.
|
||||||
*/
|
*/
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitBadge
|
bitBadge
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
(click)="doAutofill(cipher)"
|
||||||
[title]="'autofillTitle' | i18n: cipher.name"
|
[title]="'autofillTitle' | i18n: cipher.name"
|
||||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||||
>
|
>
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||||
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
||||||
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
||||||
@ -87,5 +88,12 @@ export class VaultListItemsContainerComponent {
|
|||||||
return cipher.collections[0]?.name;
|
return cipher.collections[0]?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private vaultPopupAutofillService: VaultPopupAutofillService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async doAutofill(cipher: PopupCipherView) {
|
||||||
|
await this.vaultPopupAutofillService.doAutofill(cipher);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { Router, RouterLink } from "@angular/router";
|
import { RouterLink } from "@angular/router";
|
||||||
import { combineLatest } from "rxjs";
|
import { combineLatest } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@ -59,10 +59,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected VaultStateEnum = VaultState;
|
protected VaultStateEnum = VaultState;
|
||||||
|
|
||||||
constructor(
|
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
|
||||||
private vaultPopupItemsService: VaultPopupItemsService,
|
|
||||||
private router: Router,
|
|
||||||
) {
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.vaultPopupItemsService.emptyVault$,
|
this.vaultPopupItemsService.emptyVault$,
|
||||||
this.vaultPopupItemsService.noFilteredResults$,
|
this.vaultPopupItemsService.noFilteredResults$,
|
||||||
|
@ -0,0 +1,356 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { subscribeTo } from "@bitwarden/common/spec";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AutoFillOptions,
|
||||||
|
AutofillService,
|
||||||
|
PageDetail,
|
||||||
|
} from "../../../autofill/services/abstractions/autofill.service";
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";
|
||||||
|
|
||||||
|
describe("VaultPopupAutofillService", () => {
|
||||||
|
let testBed: TestBed;
|
||||||
|
let service: VaultPopupAutofillService;
|
||||||
|
|
||||||
|
const mockCurrentTab = { url: "https://example.com" } as chrome.tabs.Tab;
|
||||||
|
|
||||||
|
// Create mocks for VaultPopupAutofillService
|
||||||
|
const mockAutofillService = mock<AutofillService>();
|
||||||
|
const mockI18nService = mock<I18nService>();
|
||||||
|
const mockToastService = mock<ToastService>();
|
||||||
|
const mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
const mockPasswordRepromptService = mock<PasswordRepromptService>();
|
||||||
|
const mockCipherService = mock<CipherService>();
|
||||||
|
const mockMessagingService = mock<MessagingService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockCurrentTab);
|
||||||
|
|
||||||
|
mockAutofillService.collectPageDetailsFromTab$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
|
||||||
|
testBed = TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AutofillService, useValue: mockAutofillService },
|
||||||
|
{ provide: I18nService, useValue: mockI18nService },
|
||||||
|
{ provide: ToastService, useValue: mockToastService },
|
||||||
|
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||||
|
{ provide: PasswordRepromptService, useValue: mockPasswordRepromptService },
|
||||||
|
{ provide: CipherService, useValue: mockCipherService },
|
||||||
|
{ provide: MessagingService, useValue: mockMessagingService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = testBed.inject(VaultPopupAutofillService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be created", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("currentAutofillTab$", () => {
|
||||||
|
it("should return null if in popout", (done) => {
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
||||||
|
service.refreshCurrentTab();
|
||||||
|
service.currentAutofillTab$.subscribe((tab) => {
|
||||||
|
expect(tab).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return BrowserApi.getTabFromCurrentWindow() if not in popout", (done) => {
|
||||||
|
service.currentAutofillTab$.subscribe((tab) => {
|
||||||
|
expect(tab).toEqual(mockCurrentTab);
|
||||||
|
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only fetch the current tab once when subscribed to multiple times", async () => {
|
||||||
|
const firstTracked = subscribeTo(service.currentAutofillTab$);
|
||||||
|
const secondTracked = subscribeTo(service.currentAutofillTab$);
|
||||||
|
|
||||||
|
await firstTracked.pauseUntilReceived(1);
|
||||||
|
await secondTracked.pauseUntilReceived(1);
|
||||||
|
|
||||||
|
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("autofillAllowed$", () => {
|
||||||
|
it("should return true if there is a current tab", (done) => {
|
||||||
|
service.autofillAllowed$.subscribe((allowed) => {
|
||||||
|
expect(allowed).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if there is no current tab", (done) => {
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
||||||
|
service.refreshCurrentTab();
|
||||||
|
service.autofillAllowed$.subscribe((allowed) => {
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if in a popout", (done) => {
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
||||||
|
service.refreshCurrentTab();
|
||||||
|
service.autofillAllowed$.subscribe((allowed) => {
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("refreshCurrentTab()", () => {
|
||||||
|
it("should refresh currentAutofillTab$", async () => {
|
||||||
|
const tracked = subscribeTo(service.currentAutofillTab$);
|
||||||
|
service.refreshCurrentTab();
|
||||||
|
await tracked.pauseUntilReceived(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("autofill methods", () => {
|
||||||
|
const mockPageDetails: PageDetail[] = [{ tab: mockCurrentTab, details: {} as any, frameId: 1 }];
|
||||||
|
let mockCipher: CipherView;
|
||||||
|
let expectedAutofillArgs: AutoFillOptions;
|
||||||
|
let mockPageDetails$: BehaviorSubject<PageDetail[]>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockCipher = new CipherView();
|
||||||
|
mockCipher.type = CipherType.Login;
|
||||||
|
|
||||||
|
mockPageDetails$ = new BehaviorSubject(mockPageDetails);
|
||||||
|
|
||||||
|
mockAutofillService.collectPageDetailsFromTab$.mockReturnValue(mockPageDetails$);
|
||||||
|
|
||||||
|
expectedAutofillArgs = {
|
||||||
|
tab: mockCurrentTab,
|
||||||
|
cipher: mockCipher,
|
||||||
|
pageDetails: mockPageDetails,
|
||||||
|
doc: expect.any(Document),
|
||||||
|
fillNewPassword: true,
|
||||||
|
allowTotpAutofill: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Refresh the current tab so the mockedPageDetails$ are used
|
||||||
|
service.refreshCurrentTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("doAutofill()", () => {
|
||||||
|
it("should return true if autofill is successful", async () => {
|
||||||
|
mockAutofillService.doAutoFill.mockResolvedValue(null);
|
||||||
|
const result = await service.doAutofill(mockCipher);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockAutofillService.doAutoFill).toHaveBeenCalledWith(expectedAutofillArgs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if autofill is not successful", async () => {
|
||||||
|
mockAutofillService.doAutoFill.mockRejectedValue(null);
|
||||||
|
const result = await service.doAutofill(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("autofillError"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if tab is null", async () => {
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
||||||
|
const result = await service.doAutofill(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("autofillError"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if missing page details", async () => {
|
||||||
|
mockPageDetails$.next([]);
|
||||||
|
const result = await service.doAutofill(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("autofillError"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show password prompt if cipher requires reprompt", async () => {
|
||||||
|
mockCipher.reprompt = CipherRepromptType.Password;
|
||||||
|
mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false);
|
||||||
|
const result = await service.doAutofill(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should copy TOTP code to clipboard if available", async () => {
|
||||||
|
const totpCode = "123456";
|
||||||
|
mockAutofillService.doAutoFill.mockResolvedValue(totpCode);
|
||||||
|
await service.doAutofill(mockCipher);
|
||||||
|
expect(mockPlatformUtilsService.copyToClipboard).toHaveBeenCalledWith(
|
||||||
|
totpCode,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("closePopup", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(BrowserApi, "closePopup").mockImplementation();
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
|
||||||
|
mockPlatformUtilsService.isFirefox.mockReturnValue(true);
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close popup by default when in popup", async () => {
|
||||||
|
await service.doAutofill(mockCipher);
|
||||||
|
expect(BrowserApi.closePopup).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not close popup when closePopup is set to false", async () => {
|
||||||
|
await service.doAutofill(mockCipher, false);
|
||||||
|
expect(BrowserApi.closePopup).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close popup after a timeout for chromium browsers", async () => {
|
||||||
|
mockPlatformUtilsService.isFirefox.mockReturnValue(false);
|
||||||
|
jest.spyOn(global, "setTimeout");
|
||||||
|
await service.doAutofill(mockCipher);
|
||||||
|
jest.advanceTimersByTime(50);
|
||||||
|
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||||
|
expect(BrowserApi.closePopup).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("doAutofillAndSave()", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mocks for service._closePopup()
|
||||||
|
jest.spyOn(BrowserApi, "closePopup").mockImplementation();
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
|
||||||
|
mockPlatformUtilsService.isFirefox.mockReturnValue(true);
|
||||||
|
|
||||||
|
// Default to happy path
|
||||||
|
mockAutofillService.doAutoFill.mockResolvedValue(null);
|
||||||
|
mockCipherService.updateWithServer.mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if cipher is not login type", async () => {
|
||||||
|
mockCipher.type = CipherType.Card;
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockAutofillService.doAutoFill).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if autofill is not successful", async () => {
|
||||||
|
mockAutofillService.doAutoFill.mockRejectedValue(null);
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("autofillError"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if the cipher already has a URI for the tab", async () => {
|
||||||
|
mockCipher.login = new LoginView();
|
||||||
|
mockCipher.login.uris = [{ uri: mockCurrentTab.url } as LoginUriView];
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(BrowserApi.closePopup).toHaveBeenCalled();
|
||||||
|
expect(mockCipherService.updateWithServer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show a success toast if closePopup is false and cipher already has URI for tab", async () => {
|
||||||
|
mockCipher.login = new LoginView();
|
||||||
|
mockCipher.login.uris = [{ uri: mockCurrentTab.url } as LoginUriView];
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher, false);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(BrowserApi.closePopup).not.toHaveBeenCalled();
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("autoFillSuccessAndSavedUri"),
|
||||||
|
});
|
||||||
|
expect(mockCipherService.updateWithServer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a URI to the cipher and save with the server", async () => {
|
||||||
|
const mockEncryptedCipher = {} as Cipher;
|
||||||
|
mockCipherService.encrypt.mockResolvedValue(mockEncryptedCipher);
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockCipher.login.uris).toHaveLength(1);
|
||||||
|
expect(mockCipher.login.uris[0].uri).toBe(mockCurrentTab.url);
|
||||||
|
expect(mockCipherService.encrypt).toHaveBeenCalledWith(mockCipher);
|
||||||
|
expect(mockCipherService.updateWithServer).toHaveBeenCalledWith(mockEncryptedCipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a URI to the cipher when there are no existing URIs", async () => {
|
||||||
|
mockCipher.login.uris = null;
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockCipher.login.uris).toHaveLength(1);
|
||||||
|
expect(mockCipher.login.uris[0].uri).toBe(mockCurrentTab.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show an error toast if saving the cipher fails", async () => {
|
||||||
|
mockCipherService.updateWithServer.mockRejectedValue(null);
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("unexpectedError"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close the popup after saving the cipher", async () => {
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(BrowserApi.closePopup).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show success toast after saving the cipher if closePop is false", async () => {
|
||||||
|
mockAutofillService.doAutoFill.mockResolvedValue(null);
|
||||||
|
const result = await service.doAutofillAndSave(mockCipher, false);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(BrowserApi.closePopup).not.toHaveBeenCalled();
|
||||||
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: mockI18nService.t("autoFillSuccessAndSavedUri"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,237 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import {
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
shareReplay,
|
||||||
|
startWith,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AutofillService,
|
||||||
|
PageDetail,
|
||||||
|
} from "../../../autofill/services/abstractions/autofill.service";
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class VaultPopupAutofillService {
|
||||||
|
private _refreshCurrentTab$ = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that contains the current tab to be considered for autofill. If there is no current tab
|
||||||
|
* or the popup is in a popout window, this will be null.
|
||||||
|
*/
|
||||||
|
currentAutofillTab$: Observable<chrome.tabs.Tab | null> = this._refreshCurrentTab$.pipe(
|
||||||
|
startWith(null),
|
||||||
|
switchMap(async () => {
|
||||||
|
if (BrowserPopupUtils.inPopout(window)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await BrowserApi.getTabFromCurrentWindow();
|
||||||
|
}),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that indicates whether autofill is allowed in the current context.
|
||||||
|
* Autofill is allowed when there is a current tab and the popup is not in a popout window.
|
||||||
|
*/
|
||||||
|
autofillAllowed$: Observable<boolean> = this.currentAutofillTab$.pipe(map((tab) => !!tab));
|
||||||
|
|
||||||
|
private _currentPageDetails$: Observable<PageDetail[]> = this.currentAutofillTab$.pipe(
|
||||||
|
switchMap((tab) => {
|
||||||
|
if (!tab) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
return this.autofillService.collectPageDetailsFromTab$(tab);
|
||||||
|
}),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private autofillService: AutofillService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private platformUtilService: PlatformUtilsService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
) {
|
||||||
|
this._currentPageDetails$.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _internalDoAutofill(
|
||||||
|
cipher: CipherView,
|
||||||
|
tab: chrome.tabs.Tab,
|
||||||
|
pageDetails: PageDetail[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (
|
||||||
|
cipher.reprompt !== CipherRepromptType.None &&
|
||||||
|
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab == null || pageDetails.length === 0) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("autofillError"),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const totpCode = await this.autofillService.doAutoFill({
|
||||||
|
tab,
|
||||||
|
cipher,
|
||||||
|
pageDetails,
|
||||||
|
doc: window.document,
|
||||||
|
fillNewPassword: true,
|
||||||
|
allowTotpAutofill: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totpCode != null) {
|
||||||
|
this.platformUtilService.copyToClipboard(totpCode, { window: window });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("autofillError"),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closePopup() {
|
||||||
|
if (!BrowserPopupUtils.inPopup(window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.platformUtilService.isFirefox() || this.platformUtilService.isSafari()) {
|
||||||
|
BrowserApi.closePopup(window);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard
|
||||||
|
setTimeout(() => BrowserApi.closePopup(window), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-fetch the current tab
|
||||||
|
*/
|
||||||
|
refreshCurrentTab() {
|
||||||
|
this._refreshCurrentTab$.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to autofill the given cipher. Returns true if the autofill was successful, false otherwise.
|
||||||
|
* Will copy any TOTP code to the clipboard if available after successful autofill.
|
||||||
|
* @param cipher
|
||||||
|
* @param closePopup If true, will close the popup window after successful autofill. Defaults to true.
|
||||||
|
*/
|
||||||
|
async doAutofill(cipher: CipherView, closePopup = true): Promise<boolean> {
|
||||||
|
const tab = await firstValueFrom(this.currentAutofillTab$);
|
||||||
|
const pageDetails = await firstValueFrom(this._currentPageDetails$);
|
||||||
|
|
||||||
|
const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails);
|
||||||
|
|
||||||
|
if (didAutofill && closePopup) {
|
||||||
|
this._closePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
return didAutofill;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to autofill the given cipher and, upon successful autofill, saves the URI to the cipher.
|
||||||
|
* Will copy any TOTP code to the clipboard if available after successful autofill.
|
||||||
|
* @param cipher The cipher to autofill and save. Only Login ciphers are supported.
|
||||||
|
* @param closePopup If true, will close the popup window after successful autofill.
|
||||||
|
* If false, will show a success toast instead. Defaults to true.
|
||||||
|
*/
|
||||||
|
async doAutofillAndSave(cipher: CipherView, closePopup = true): Promise<boolean> {
|
||||||
|
// We can only save URIs for login ciphers
|
||||||
|
if (cipher.type !== CipherType.Login) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageDetails = await firstValueFrom(this._currentPageDetails$);
|
||||||
|
const tab = await firstValueFrom(this.currentAutofillTab$);
|
||||||
|
|
||||||
|
const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails);
|
||||||
|
|
||||||
|
if (!didAutofill) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const didSaveUri = await this._saveNewUri(cipher, tab);
|
||||||
|
|
||||||
|
if (!didSaveUri) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closePopup) {
|
||||||
|
this._closePopup();
|
||||||
|
} else {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("autoFillSuccessAndSavedUri"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the current tab's URL as a new URI for the given cipher. If the cipher already has a URI for the tab,
|
||||||
|
* this method does nothing and returns true.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async _saveNewUri(cipher: CipherView, tab: chrome.tabs.Tab): Promise<boolean> {
|
||||||
|
cipher.login.uris ??= [];
|
||||||
|
|
||||||
|
if (cipher.login.uris.some((uri) => uri.uri === tab.url)) {
|
||||||
|
// Cipher already has a URI for this tab
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginUri = new LoginUriView();
|
||||||
|
loginUri.uri = tab.url;
|
||||||
|
cipher.login.uris.push(loginUri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encCipher = await this.cipherService.encrypt(cipher);
|
||||||
|
await this.cipherService.updateWithServer(encCipher);
|
||||||
|
this.messagingService.send("editedCipher");
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("unexpectedError"),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,8 +16,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
|
||||||
|
|
||||||
|
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";
|
||||||
import { VaultPopupItemsService } from "./vault-popup-items.service";
|
import { VaultPopupItemsService } from "./vault-popup-items.service";
|
||||||
import { VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
import { VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
const vaultPopupListFiltersServiceMock = mock<VaultPopupListFiltersService>();
|
const vaultPopupListFiltersServiceMock = mock<VaultPopupListFiltersService>();
|
||||||
const searchService = mock<SearchService>();
|
const searchService = mock<SearchService>();
|
||||||
const collectionService = mock<CollectionService>();
|
const collectionService = mock<CollectionService>();
|
||||||
|
const vaultAutofillServiceMock = mock<VaultPopupAutofillService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
allCiphers = cipherFactory(10);
|
allCiphers = cipherFactory(10);
|
||||||
@ -70,10 +71,10 @@ describe("VaultPopupItemsService", () => {
|
|||||||
vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject(
|
vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject(
|
||||||
(ciphers: CipherView[]) => ciphers,
|
(ciphers: CipherView[]) => ciphers,
|
||||||
);
|
);
|
||||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
|
||||||
jest
|
vaultAutofillServiceMock.currentAutofillTab$ = new BehaviorSubject({
|
||||||
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
url: "https://example.com",
|
||||||
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
} as chrome.tabs.Tab);
|
||||||
|
|
||||||
mockOrg = {
|
mockOrg = {
|
||||||
id: "org1",
|
id: "org1",
|
||||||
@ -97,6 +98,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
{ provide: OrganizationService, useValue: organizationServiceMock },
|
{ provide: OrganizationService, useValue: organizationServiceMock },
|
||||||
{ provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock },
|
{ provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock },
|
||||||
{ provide: CollectionService, useValue: collectionService },
|
{ provide: CollectionService, useValue: collectionService },
|
||||||
|
{ provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,15 +157,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
|
|
||||||
describe("autoFillCiphers$", () => {
|
describe("autoFillCiphers$", () => {
|
||||||
it("should return empty array if there is no current tab", (done) => {
|
it("should return empty array if there is no current tab", (done) => {
|
||||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
(vaultAutofillServiceMock.currentAutofillTab$ as BehaviorSubject<any>).next(null);
|
||||||
service.autoFillCiphers$.subscribe((ciphers) => {
|
|
||||||
expect(ciphers).toEqual([]);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty array if in Popout window", (done) => {
|
|
||||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
|
||||||
service.autoFillCiphers$.subscribe((ciphers) => {
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
expect(ciphers).toEqual([]);
|
expect(ciphers).toEqual([]);
|
||||||
done();
|
done();
|
||||||
@ -319,31 +313,6 @@ describe("VaultPopupItemsService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("autoFillAllowed$", () => {
|
|
||||||
it("should return true if there is a current tab", (done) => {
|
|
||||||
service.autofillAllowed$.subscribe((allowed) => {
|
|
||||||
expect(allowed).toBe(true);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false if there is no current tab", (done) => {
|
|
||||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
|
||||||
service.autofillAllowed$.subscribe((allowed) => {
|
|
||||||
expect(allowed).toBe(false);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false if in a Popout", (done) => {
|
|
||||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
|
||||||
service.autofillAllowed$.subscribe((allowed) => {
|
|
||||||
expect(allowed).toBe(false);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("noFilteredResults$", () => {
|
describe("noFilteredResults$", () => {
|
||||||
it("should return false when filteredResults has values", (done) => {
|
it("should return false when filteredResults has values", (done) => {
|
||||||
service.noFilteredResults$.subscribe((noResults) => {
|
service.noFilteredResults$.subscribe((noResults) => {
|
||||||
|
@ -28,11 +28,10 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault
|
|||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
|
||||||
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
|
||||||
import { PopupCipherView } from "../views/popup-cipher.view";
|
import { PopupCipherView } from "../views/popup-cipher.view";
|
||||||
|
|
||||||
|
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";
|
||||||
import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,7 +41,6 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi
|
|||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class VaultPopupItemsService {
|
export class VaultPopupItemsService {
|
||||||
private _refreshCurrentTab$ = new Subject<void>();
|
|
||||||
private _searchText$ = new BehaviorSubject<string>("");
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,22 +68,6 @@ export class VaultPopupItemsService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Observable that contains the current tab to be considered for autofill. If there is no current tab
|
|
||||||
* or the popup is in a popout window, this will be null.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private _currentAutofillTab$: Observable<chrome.tabs.Tab | null> = this._refreshCurrentTab$.pipe(
|
|
||||||
startWith(null),
|
|
||||||
switchMap(async () => {
|
|
||||||
if (BrowserPopupUtils.inPopout(window)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return await BrowserApi.getTabFromCurrentWindow();
|
|
||||||
}),
|
|
||||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that contains the list of all decrypted ciphers.
|
* Observable that contains the list of all decrypted ciphers.
|
||||||
* @private
|
* @private
|
||||||
@ -145,7 +127,7 @@ export class VaultPopupItemsService {
|
|||||||
autoFillCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
autoFillCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
||||||
this._filteredCipherList$,
|
this._filteredCipherList$,
|
||||||
this._otherAutoFillTypes$,
|
this._otherAutoFillTypes$,
|
||||||
this._currentAutofillTab$,
|
this.vaultPopupAutofillService.currentAutofillTab$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(([ciphers, otherTypes, tab]) => {
|
switchMap(([ciphers, otherTypes, tab]) => {
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
@ -217,12 +199,6 @@ export class VaultPopupItemsService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Observable that indicates whether autofill is allowed in the current context.
|
|
||||||
* Autofill is allowed when there is a current tab and the popup is not in a popout window.
|
|
||||||
*/
|
|
||||||
autofillAllowed$: Observable<boolean> = this._currentAutofillTab$.pipe(map((tab) => !!tab));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that indicates whether the user's vault is empty.
|
* Observable that indicates whether the user's vault is empty.
|
||||||
*/
|
*/
|
||||||
@ -257,15 +233,9 @@ export class VaultPopupItemsService {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
|
private vaultPopupAutofillService: VaultPopupAutofillService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-fetch the current tab to trigger a re-evaluation of the autofill ciphers.
|
|
||||||
*/
|
|
||||||
refreshCurrentTab() {
|
|
||||||
this._refreshCurrentTab$.next(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter(newSearchText: string) {
|
applyFilter(newSearchText: string) {
|
||||||
this._searchText$.next(newSearchText);
|
this._searchText$.next(newSearchText);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user