mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[PM-3455] Allow adding and autofilling Cards and Identities via Context Menu (#6050)
* PoC autofill card and identity from context menu * PoC trigger identity and card autofills via messages * update card and identity cipher titles in the context menu * remove unused url argument from loadOptions * do not show no logins message for card and identity sub-menu * allow context menu actions to create identity or card ciphers * open new single-action windows for cipher creation when requested from the context menu * add context menu items for adding a login cipher when none are available to the page * adjust titles for Card and Identity context menu items * fix translations and add no ciphers available messages to submenus * cleanup and update tests * remove unrelated changes * pass uri of context menu page to cipher creation view * Apply suggestions from code review Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> * cleanup * handle cipher edit background messages with browserPopoutWindowService as well * consolidate doAutoFillNonLoginActiveTab into doAutoFillActiveTab * cleanup --------- Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>
This commit is contained in:
parent
be8f522aac
commit
a42cea8570
@ -91,6 +91,15 @@
|
||||
"autoFill": {
|
||||
"message": "Auto-fill"
|
||||
},
|
||||
"autoFillLogin": {
|
||||
"message": "Auto-fill login"
|
||||
},
|
||||
"autoFillCard": {
|
||||
"message": "Auto-fill card"
|
||||
},
|
||||
"autoFillIdentity": {
|
||||
"message": "Auto-fill identity"
|
||||
},
|
||||
"generatePasswordCopied": {
|
||||
"message": "Generate password (copied)"
|
||||
},
|
||||
@ -100,6 +109,21 @@
|
||||
"noMatchingLogins": {
|
||||
"message": "No matching logins"
|
||||
},
|
||||
"noCards": {
|
||||
"message": "No cards"
|
||||
},
|
||||
"noIdentities": {
|
||||
"message": "No identities"
|
||||
},
|
||||
"addLoginMenu": {
|
||||
"message": "Add login"
|
||||
},
|
||||
"addCardMenu": {
|
||||
"message": "Add card"
|
||||
},
|
||||
"addIdentityMenu": {
|
||||
"message": "Add identity"
|
||||
},
|
||||
"unlockVaultMenu": {
|
||||
"message": "Unlock your vault"
|
||||
},
|
||||
|
@ -69,19 +69,20 @@ describe("CipherContextMenuHandler", () => {
|
||||
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("only adds login ciphers including ciphers that require reprompt", async () => {
|
||||
it("only adds autofill ciphers including ciphers that require reprompt", async () => {
|
||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||
|
||||
mainContextMenuHandler.init.mockResolvedValue(true);
|
||||
|
||||
const realCipher = {
|
||||
const loginCipher = {
|
||||
id: "5",
|
||||
type: CipherType.Login,
|
||||
reprompt: CipherRepromptType.None,
|
||||
name: "Test Cipher",
|
||||
login: { username: "Test Username" },
|
||||
};
|
||||
const repromptCipher = {
|
||||
|
||||
const repromptLoginCipher = {
|
||||
id: "6",
|
||||
type: CipherType.Login,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
@ -89,34 +90,49 @@ describe("CipherContextMenuHandler", () => {
|
||||
login: { username: "Test Username" },
|
||||
};
|
||||
|
||||
const cardCipher = {
|
||||
id: "7",
|
||||
type: CipherType.Card,
|
||||
name: "Test Card Cipher",
|
||||
card: { username: "Test Username" },
|
||||
};
|
||||
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([
|
||||
null, // invalid cipher
|
||||
undefined, // invalid cipher
|
||||
{ type: CipherType.Card }, // invalid cipher
|
||||
realCipher, // valid cipher
|
||||
repromptCipher,
|
||||
{ type: CipherType.SecureNote }, // invalid cipher
|
||||
loginCipher, // valid cipher
|
||||
repromptLoginCipher,
|
||||
cardCipher, // valid cipher
|
||||
] as any[]);
|
||||
|
||||
await sut.update("https://test.com");
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", [
|
||||
CipherType.Card,
|
||||
CipherType.Identity,
|
||||
]);
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||
"Test Cipher (Test Username)",
|
||||
"5",
|
||||
"https://test.com",
|
||||
realCipher
|
||||
loginCipher
|
||||
);
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||
"Test Reprompt Cipher (Test Username)",
|
||||
"6",
|
||||
"https://test.com",
|
||||
repromptCipher
|
||||
repromptLoginCipher
|
||||
);
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||
"Test Card Cipher",
|
||||
"7",
|
||||
cardCipher
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
cipherServiceFactory,
|
||||
CipherServiceInitOptions,
|
||||
} from "../../vault/background/service_factories/cipher-service.factory";
|
||||
import { AutofillCipherTypeId } from "../types";
|
||||
|
||||
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
||||
|
||||
@ -159,29 +160,67 @@ export class CipherContextMenuHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(url);
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(url, [
|
||||
CipherType.Card,
|
||||
CipherType.Identity,
|
||||
]);
|
||||
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||
|
||||
if (ciphers.length === 0) {
|
||||
await this.mainContextMenuHandler.noLogins(url);
|
||||
return;
|
||||
const groupedCiphers: Record<AutofillCipherTypeId, CipherView[]> = ciphers.reduce(
|
||||
(ciphersByType, cipher) => {
|
||||
if (!cipher?.type) {
|
||||
return ciphersByType;
|
||||
}
|
||||
|
||||
const existingCiphersOfType = ciphersByType[cipher.type as AutofillCipherTypeId] || [];
|
||||
|
||||
return {
|
||||
...ciphersByType,
|
||||
[cipher.type]: [...existingCiphersOfType, cipher],
|
||||
};
|
||||
},
|
||||
{
|
||||
[CipherType.Login]: [],
|
||||
[CipherType.Card]: [],
|
||||
[CipherType.Identity]: [],
|
||||
}
|
||||
);
|
||||
|
||||
if (groupedCiphers[CipherType.Login].length === 0) {
|
||||
await this.mainContextMenuHandler.noLogins();
|
||||
}
|
||||
|
||||
if (groupedCiphers[CipherType.Identity].length === 0) {
|
||||
await this.mainContextMenuHandler.noIdentities();
|
||||
}
|
||||
|
||||
if (groupedCiphers[CipherType.Card].length === 0) {
|
||||
await this.mainContextMenuHandler.noCards();
|
||||
}
|
||||
|
||||
for (const cipher of ciphers) {
|
||||
await this.updateForCipher(url, cipher);
|
||||
await this.updateForCipher(cipher);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateForCipher(url: string, cipher: CipherView) {
|
||||
if (cipher == null || cipher.type !== CipherType.Login) {
|
||||
private async updateForCipher(cipher: CipherView) {
|
||||
if (
|
||||
cipher == null ||
|
||||
!new Set([CipherType.Login, CipherType.Card, CipherType.Identity]).has(cipher.type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = cipher.name;
|
||||
if (!Utils.isNullOrEmpty(title)) {
|
||||
|
||||
if (cipher.type === CipherType.Login && !Utils.isNullOrEmpty(title) && cipher.login?.username) {
|
||||
title += ` (${cipher.login.username})`;
|
||||
}
|
||||
|
||||
await this.mainContextMenuHandler.loadOptions(title, cipher.id, url, cipher);
|
||||
if (cipher.type === CipherType.Card && cipher.card?.subTitle) {
|
||||
title += ` ${cipher.card.subTitle}`;
|
||||
}
|
||||
|
||||
await this.mainContextMenuHandler.loadOptions(title, cipher.id, cipher);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,15 @@ import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import {
|
||||
AUTOFILL_ID,
|
||||
COPY_PASSWORD_ID,
|
||||
COPY_USERNAME_ID,
|
||||
COPY_VERIFICATIONCODE_ID,
|
||||
GENERATE_PASSWORD_ID,
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
} from "../constants";
|
||||
|
||||
import {
|
||||
CopyToClipboardAction,
|
||||
ContextMenuClickedHandler,
|
||||
@ -17,13 +26,6 @@ import {
|
||||
GeneratePasswordToClipboardAction,
|
||||
AutofillAction,
|
||||
} from "./context-menu-clicked-handler";
|
||||
import {
|
||||
AUTOFILL_ID,
|
||||
COPY_PASSWORD_ID,
|
||||
COPY_USERNAME_ID,
|
||||
COPY_VERIFICATIONCODE_ID,
|
||||
GENERATE_PASSWORD_ID,
|
||||
} from "./main-context-menu-handler";
|
||||
|
||||
describe("ContextMenuClickedHandler", () => {
|
||||
const createData = (
|
||||
@ -51,6 +53,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||
type: CipherType.Login,
|
||||
} as any)
|
||||
);
|
||||
|
||||
cipherView.login.username = username ?? "USERNAME";
|
||||
cipherView.login.password = password ?? "PASSWORD";
|
||||
cipherView.login.totp = totp ?? "TOTP";
|
||||
@ -106,7 +109,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||
const cipher = createCipher();
|
||||
cipherService.getAllDecrypted.mockResolvedValue([cipher]);
|
||||
|
||||
await sut.run(createData("T_1", AUTOFILL_ID), { id: 5 } as any);
|
||||
await sut.run(createData(`${AUTOFILL_ID}_1`, AUTOFILL_ID), { id: 5 } as any);
|
||||
|
||||
expect(autofill).toBeCalledTimes(1);
|
||||
|
||||
@ -118,11 +121,16 @@ describe("ContextMenuClickedHandler", () => {
|
||||
createCipher({ username: "TEST_USERNAME" }),
|
||||
]);
|
||||
|
||||
await sut.run(createData("T_1", COPY_USERNAME_ID));
|
||||
await sut.run(createData(`${COPY_USERNAME_ID}_1`, COPY_USERNAME_ID), {
|
||||
url: "https://test.com",
|
||||
} as any);
|
||||
|
||||
expect(copyToClipboard).toBeCalledTimes(1);
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith({ text: "TEST_USERNAME", options: undefined });
|
||||
expect(copyToClipboard).toHaveBeenCalledWith({
|
||||
text: "TEST_USERNAME",
|
||||
tab: { url: "https://test.com" },
|
||||
});
|
||||
});
|
||||
|
||||
it("copies password to clipboard", async () => {
|
||||
@ -130,11 +138,16 @@ describe("ContextMenuClickedHandler", () => {
|
||||
createCipher({ password: "TEST_PASSWORD" }),
|
||||
]);
|
||||
|
||||
await sut.run(createData("T_1", COPY_PASSWORD_ID));
|
||||
await sut.run(createData(`${COPY_PASSWORD_ID}_1`, COPY_PASSWORD_ID), {
|
||||
url: "https://test.com",
|
||||
} as any);
|
||||
|
||||
expect(copyToClipboard).toBeCalledTimes(1);
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith({ text: "TEST_PASSWORD", options: undefined });
|
||||
expect(copyToClipboard).toHaveBeenCalledWith({
|
||||
text: "TEST_PASSWORD",
|
||||
tab: { url: "https://test.com" },
|
||||
});
|
||||
});
|
||||
|
||||
it("copies totp code to clipboard", async () => {
|
||||
@ -148,11 +161,16 @@ describe("ContextMenuClickedHandler", () => {
|
||||
return Promise.resolve("654321");
|
||||
});
|
||||
|
||||
await sut.run(createData("T_1", COPY_VERIFICATIONCODE_ID));
|
||||
await sut.run(createData(`${COPY_VERIFICATIONCODE_ID}_1`, COPY_VERIFICATIONCODE_ID), {
|
||||
url: "https://test.com",
|
||||
} as any);
|
||||
|
||||
expect(totpService.getCode).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith({ text: "123456" });
|
||||
expect(copyToClipboard).toHaveBeenCalledWith({
|
||||
text: "123456",
|
||||
tab: { url: "https://test.com" },
|
||||
});
|
||||
});
|
||||
|
||||
it("attempts to find a cipher when noop but unlocked", async () => {
|
||||
@ -163,11 +181,13 @@ describe("ContextMenuClickedHandler", () => {
|
||||
} as any,
|
||||
]);
|
||||
|
||||
await sut.run(createData("T_noop", COPY_USERNAME_ID), { url: "https://test.com" } as any);
|
||||
await sut.run(createData(`${COPY_USERNAME_ID}_${NOOP_COMMAND_SUFFIX}`, COPY_USERNAME_ID), {
|
||||
url: "https://test.com",
|
||||
} as any);
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []);
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledTimes(1);
|
||||
|
||||
@ -185,11 +205,13 @@ describe("ContextMenuClickedHandler", () => {
|
||||
} as any,
|
||||
]);
|
||||
|
||||
await sut.run(createData("T_noop", COPY_USERNAME_ID), { url: "https://test.com" } as any);
|
||||
await sut.run(createData(`${COPY_USERNAME_ID}_${NOOP_COMMAND_SUFFIX}`, COPY_USERNAME_ID), {
|
||||
url: "https://test.com",
|
||||
} as any);
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,6 +8,7 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import {
|
||||
@ -30,16 +31,21 @@ import {
|
||||
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
||||
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
|
||||
import { AutofillTabCommand } from "../commands/autofill-tab-command";
|
||||
|
||||
import {
|
||||
AUTOFILL_CARD_ID,
|
||||
AUTOFILL_ID,
|
||||
AUTOFILL_IDENTITY_ID,
|
||||
COPY_IDENTIFIER_ID,
|
||||
COPY_PASSWORD_ID,
|
||||
COPY_USERNAME_ID,
|
||||
COPY_VERIFICATIONCODE_ID,
|
||||
CREATE_CARD_ID,
|
||||
CREATE_IDENTITY_ID,
|
||||
CREATE_LOGIN_ID,
|
||||
GENERATE_PASSWORD_ID,
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
} from "./main-context-menu-handler";
|
||||
} from "../constants";
|
||||
import { AutofillCipherTypeId } from "../types";
|
||||
|
||||
export type CopyToClipboardOptions = { text: string; tab: chrome.tabs.Tab };
|
||||
export type CopyToClipboardAction = (options: CopyToClipboardOptions) => void;
|
||||
@ -142,18 +148,16 @@ export class ContextMenuClickedHandler {
|
||||
);
|
||||
}
|
||||
|
||||
async run(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
|
||||
async run(info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) {
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case GENERATE_PASSWORD_ID:
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
await this.generatePasswordToClipboard(tab);
|
||||
break;
|
||||
case COPY_IDENTIFIER_ID:
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab });
|
||||
break;
|
||||
default:
|
||||
@ -161,7 +165,11 @@ export class ContextMenuClickedHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async cipherAction(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
|
||||
async cipherAction(info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) {
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
const retryMessage: LockedVaultPendingNotificationsItem = {
|
||||
commandToRetry: {
|
||||
@ -182,32 +190,57 @@ export class ContextMenuClickedHandler {
|
||||
|
||||
// NOTE: We don't actually use the first part of this ID, we further switch based on the parentMenuItemId
|
||||
// I would really love to not add it but that is a departure from how it currently works.
|
||||
const id = (info.menuItemId as string).split("_")[1]; // We create all the ids, we can guarantee they are strings
|
||||
const menuItemId = (info.menuItemId as string).split("_")[1]; // We create all the ids, we can guarantee they are strings
|
||||
let cipher: CipherView | undefined;
|
||||
if (id === NOOP_COMMAND_SUFFIX) {
|
||||
const isCreateCipherAction = [CREATE_LOGIN_ID, CREATE_IDENTITY_ID, CREATE_CARD_ID].includes(
|
||||
menuItemId as string
|
||||
);
|
||||
|
||||
if (isCreateCipherAction) {
|
||||
// pass; defer to logic below
|
||||
} else if (menuItemId === NOOP_COMMAND_SUFFIX) {
|
||||
const additionalCiphersToGet =
|
||||
info.parentMenuItemId === AUTOFILL_IDENTITY_ID
|
||||
? [CipherType.Identity]
|
||||
: info.parentMenuItemId === AUTOFILL_CARD_ID
|
||||
? [CipherType.Card]
|
||||
: [];
|
||||
|
||||
// This NOOP item has come through which is generally only for no access state but since we got here
|
||||
// we are actually unlocked we will do our best to find a good match of an item to autofill this is useful
|
||||
// in scenarios like unlock on autofill
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
tab.url,
|
||||
additionalCiphersToGet
|
||||
);
|
||||
|
||||
cipher = ciphers[0];
|
||||
} else {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
cipher = ciphers.find((c) => c.id === id);
|
||||
cipher = ciphers.find(({ id }) => id === menuItemId);
|
||||
}
|
||||
|
||||
if (cipher == null) {
|
||||
if (!cipher && !isCreateCipherAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info.parentMenuItemId) {
|
||||
case AUTOFILL_ID:
|
||||
if (tab == null) {
|
||||
return;
|
||||
case AUTOFILL_IDENTITY_ID:
|
||||
case AUTOFILL_CARD_ID: {
|
||||
const cipherType = this.getCipherCreationType(menuItemId);
|
||||
|
||||
if (cipherType) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
// The action here is passed on to the single-use reprompt window and doesn't change based on cipher type
|
||||
action: AUTOFILL_ID,
|
||||
});
|
||||
} else {
|
||||
@ -215,14 +248,29 @@ export class ContextMenuClickedHandler {
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case COPY_USERNAME_ID:
|
||||
if (menuItemId === CREATE_LOGIN_ID) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType: CipherType.Login,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.copyToClipboard({ text: cipher.login.username, tab: tab });
|
||||
break;
|
||||
case COPY_PASSWORD_ID:
|
||||
if (menuItemId === CREATE_LOGIN_ID) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType: CipherType.Login,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
action: COPY_PASSWORD_ID,
|
||||
action: info.parentMenuItemId,
|
||||
});
|
||||
} else {
|
||||
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
||||
@ -231,10 +279,17 @@ export class ContextMenuClickedHandler {
|
||||
|
||||
break;
|
||||
case COPY_VERIFICATIONCODE_ID:
|
||||
if (menuItemId === CREATE_LOGIN_ID) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType: CipherType.Login,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
action: COPY_VERIFICATIONCODE_ID,
|
||||
action: info.parentMenuItemId,
|
||||
});
|
||||
} else {
|
||||
this.copyToClipboard({
|
||||
@ -254,6 +309,16 @@ export class ContextMenuClickedHandler {
|
||||
);
|
||||
}
|
||||
|
||||
private getCipherCreationType(menuItemId?: string): AutofillCipherTypeId | null {
|
||||
return menuItemId === CREATE_IDENTITY_ID
|
||||
? CipherType.Identity
|
||||
: menuItemId === CREATE_CARD_ID
|
||||
? CipherType.Card
|
||||
: menuItemId === CREATE_LOGIN_ID
|
||||
? CipherType.Login
|
||||
: null;
|
||||
}
|
||||
|
||||
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
BrowserApi.sendTabsMessage(
|
||||
|
@ -60,7 +60,7 @@ describe("context-menu", () => {
|
||||
|
||||
const createdMenu = await sut.init();
|
||||
expect(createdMenu).toBeTruthy();
|
||||
expect(createSpy).toHaveBeenCalledTimes(7);
|
||||
expect(createSpy).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it("has menu enabled and has premium", async () => {
|
||||
@ -70,7 +70,7 @@ describe("context-menu", () => {
|
||||
|
||||
const createdMenu = await sut.init();
|
||||
expect(createdMenu).toBeTruthy();
|
||||
expect(createSpy).toHaveBeenCalledTimes(8);
|
||||
expect(createSpy).toHaveBeenCalledTimes(11);
|
||||
});
|
||||
});
|
||||
|
||||
@ -97,7 +97,7 @@ describe("context-menu", () => {
|
||||
};
|
||||
|
||||
it("is not a login cipher", async () => {
|
||||
await sut.loadOptions("TEST_TITLE", "1", "", {
|
||||
await sut.loadOptions("TEST_TITLE", "1", {
|
||||
...createCipher(),
|
||||
type: CipherType.SecureNote,
|
||||
} as any);
|
||||
@ -109,7 +109,6 @@ describe("context-menu", () => {
|
||||
await sut.loadOptions(
|
||||
"TEST_TITLE",
|
||||
"1",
|
||||
"",
|
||||
createCipher({
|
||||
username: "",
|
||||
totp: "",
|
||||
@ -123,18 +122,18 @@ describe("context-menu", () => {
|
||||
it("create entry for each cipher piece", async () => {
|
||||
stateService.getCanAccessPremium.mockResolvedValue(true);
|
||||
|
||||
await sut.loadOptions("TEST_TITLE", "1", "", createCipher());
|
||||
await sut.loadOptions("TEST_TITLE", "1", createCipher());
|
||||
|
||||
// One for autofill, copy username, copy password, and copy totp code
|
||||
expect(createSpy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("creates noop item for no cipher", async () => {
|
||||
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
|
||||
stateService.getCanAccessPremium.mockResolvedValue(true);
|
||||
|
||||
await sut.loadOptions("TEST_TITLE", "NOOP", "");
|
||||
await sut.loadOptions("TEST_TITLE", "NOOP");
|
||||
|
||||
expect(createSpy).toHaveBeenCalledTimes(4);
|
||||
expect(createSpy).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -21,22 +21,24 @@ import {
|
||||
StateServiceInitOptions,
|
||||
} from "../../platform/background/service-factories/state-service.factory";
|
||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||
|
||||
export const ROOT_ID = "root";
|
||||
|
||||
export const AUTOFILL_ID = "autofill";
|
||||
export const COPY_USERNAME_ID = "copy-username";
|
||||
export const COPY_PASSWORD_ID = "copy-password";
|
||||
export const COPY_VERIFICATIONCODE_ID = "copy-totp";
|
||||
export const COPY_IDENTIFIER_ID = "copy-identifier";
|
||||
|
||||
const SEPARATOR_ID = "separator";
|
||||
export const GENERATE_PASSWORD_ID = "generate-password";
|
||||
|
||||
export const NOOP_COMMAND_SUFFIX = "noop";
|
||||
import {
|
||||
AUTOFILL_CARD_ID,
|
||||
AUTOFILL_ID,
|
||||
AUTOFILL_IDENTITY_ID,
|
||||
COPY_IDENTIFIER_ID,
|
||||
COPY_PASSWORD_ID,
|
||||
COPY_USERNAME_ID,
|
||||
COPY_VERIFICATIONCODE_ID,
|
||||
CREATE_CARD_ID,
|
||||
CREATE_IDENTITY_ID,
|
||||
CREATE_LOGIN_ID,
|
||||
GENERATE_PASSWORD_ID,
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
ROOT_ID,
|
||||
SEPARATOR_ID,
|
||||
} from "../constants";
|
||||
|
||||
export class MainContextMenuHandler {
|
||||
//
|
||||
private initRunning = false;
|
||||
|
||||
create: (options: chrome.contextMenus.CreateProperties) => Promise<void>;
|
||||
@ -120,7 +122,7 @@ export class MainContextMenuHandler {
|
||||
await create({
|
||||
id: AUTOFILL_ID,
|
||||
parentId: ROOT_ID,
|
||||
title: this.i18nService.t("autoFill"),
|
||||
title: this.i18nService.t("autoFillLogin"),
|
||||
});
|
||||
|
||||
await create({
|
||||
@ -144,7 +146,25 @@ export class MainContextMenuHandler {
|
||||
}
|
||||
|
||||
await create({
|
||||
id: SEPARATOR_ID,
|
||||
id: SEPARATOR_ID + 1,
|
||||
type: "separator",
|
||||
parentId: ROOT_ID,
|
||||
});
|
||||
|
||||
await create({
|
||||
id: AUTOFILL_IDENTITY_ID,
|
||||
parentId: ROOT_ID,
|
||||
title: this.i18nService.t("autoFillIdentity"),
|
||||
});
|
||||
|
||||
await create({
|
||||
id: AUTOFILL_CARD_ID,
|
||||
parentId: ROOT_ID,
|
||||
title: this.i18nService.t("autoFillCard"),
|
||||
});
|
||||
|
||||
await create({
|
||||
id: SEPARATOR_ID + 2,
|
||||
type: "separator",
|
||||
parentId: ROOT_ID,
|
||||
});
|
||||
@ -194,40 +214,52 @@ export class MainContextMenuHandler {
|
||||
});
|
||||
}
|
||||
|
||||
async loadOptions(title: string, id: string, url: string, cipher?: CipherView | undefined) {
|
||||
if (cipher != null && cipher.type !== CipherType.Login) {
|
||||
return;
|
||||
}
|
||||
|
||||
async loadOptions(title: string, optionId: string, cipher?: CipherView) {
|
||||
try {
|
||||
const sanitizedTitle = MainContextMenuHandler.sanitizeContextMenuTitle(title);
|
||||
|
||||
const createChildItem = async (parent: string) => {
|
||||
const menuItemId = `${parent}_${id}`;
|
||||
const createChildItem = async (parentId: string) => {
|
||||
const menuItemId = `${parentId}_${optionId}`;
|
||||
|
||||
return await this.create({
|
||||
type: "normal",
|
||||
id: menuItemId,
|
||||
parentId: parent,
|
||||
parentId,
|
||||
title: sanitizedTitle,
|
||||
contexts: ["all"],
|
||||
});
|
||||
};
|
||||
|
||||
if (cipher == null || !Utils.isNullOrEmpty(cipher.login.password)) {
|
||||
if (
|
||||
!cipher ||
|
||||
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
|
||||
) {
|
||||
await createChildItem(AUTOFILL_ID);
|
||||
|
||||
if (cipher?.viewPassword ?? true) {
|
||||
await createChildItem(COPY_PASSWORD_ID);
|
||||
}
|
||||
}
|
||||
|
||||
if (cipher == null || !Utils.isNullOrEmpty(cipher.login.username)) {
|
||||
if (
|
||||
!cipher ||
|
||||
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.username))
|
||||
) {
|
||||
await createChildItem(COPY_USERNAME_ID);
|
||||
}
|
||||
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
if (canAccessPremium && (cipher == null || !Utils.isNullOrEmpty(cipher.login.totp))) {
|
||||
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
|
||||
await createChildItem(COPY_VERIFICATIONCODE_ID);
|
||||
}
|
||||
|
||||
if ((!cipher || cipher.type === CipherType.Card) && optionId !== CREATE_LOGIN_ID) {
|
||||
await createChildItem(AUTOFILL_CARD_ID);
|
||||
}
|
||||
|
||||
if ((!cipher || cipher.type === CipherType.Identity) && optionId !== CREATE_LOGIN_ID) {
|
||||
await createChildItem(AUTOFILL_IDENTITY_ID);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
@ -242,13 +274,72 @@ export class MainContextMenuHandler {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
await this.loadOptions(
|
||||
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
"<all_urls>"
|
||||
NOOP_COMMAND_SUFFIX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async noLogins(url: string) {
|
||||
await this.loadOptions(this.i18nService.t("noMatchingLogins"), NOOP_COMMAND_SUFFIX, url);
|
||||
async noCards() {
|
||||
await this.create({
|
||||
id: `${AUTOFILL_CARD_ID}_NOTICE`,
|
||||
enabled: false,
|
||||
parentId: AUTOFILL_CARD_ID,
|
||||
title: this.i18nService.t("noCards"),
|
||||
type: "normal",
|
||||
});
|
||||
|
||||
await this.create({
|
||||
id: `${AUTOFILL_CARD_ID}_${SEPARATOR_ID}`,
|
||||
parentId: AUTOFILL_CARD_ID,
|
||||
type: "separator",
|
||||
});
|
||||
|
||||
await this.create({
|
||||
id: `${AUTOFILL_CARD_ID}_${CREATE_CARD_ID}`,
|
||||
parentId: AUTOFILL_CARD_ID,
|
||||
title: this.i18nService.t("addCardMenu"),
|
||||
type: "normal",
|
||||
});
|
||||
}
|
||||
|
||||
async noIdentities() {
|
||||
await this.create({
|
||||
id: `${AUTOFILL_IDENTITY_ID}_NOTICE`,
|
||||
enabled: false,
|
||||
parentId: AUTOFILL_IDENTITY_ID,
|
||||
title: this.i18nService.t("noIdentities"),
|
||||
type: "normal",
|
||||
});
|
||||
|
||||
await this.create({
|
||||
id: `${AUTOFILL_IDENTITY_ID}_${SEPARATOR_ID}`,
|
||||
parentId: AUTOFILL_IDENTITY_ID,
|
||||
type: "separator",
|
||||
});
|
||||
|
||||
await this.create({
|
||||
id: `${AUTOFILL_IDENTITY_ID}_${CREATE_IDENTITY_ID}`,
|
||||
parentId: AUTOFILL_IDENTITY_ID,
|
||||
title: this.i18nService.t("addIdentityMenu"),
|
||||
type: "normal",
|
||||
});
|
||||
}
|
||||
|
||||
async noLogins() {
|
||||
await this.create({
|
||||
id: `${AUTOFILL_ID}_NOTICE`,
|
||||
enabled: false,
|
||||
parentId: AUTOFILL_ID,
|
||||
title: this.i18nService.t("noMatchingLogins"),
|
||||
type: "normal",
|
||||
});
|
||||
|
||||
await this.create({
|
||||
id: `${AUTOFILL_ID}_${SEPARATOR_ID}` + 1,
|
||||
parentId: AUTOFILL_ID,
|
||||
type: "separator",
|
||||
});
|
||||
|
||||
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
||||
}
|
||||
}
|
||||
|
@ -11,3 +11,19 @@ export const EVENTS = {
|
||||
KEYPRESS: "keypress",
|
||||
KEYUP: "keyup",
|
||||
} as const;
|
||||
|
||||
/* Context Menu item Ids */
|
||||
export const AUTOFILL_CARD_ID = "autofill-card";
|
||||
export const AUTOFILL_ID = "autofill";
|
||||
export const AUTOFILL_IDENTITY_ID = "autofill-identity";
|
||||
export const COPY_IDENTIFIER_ID = "copy-identifier";
|
||||
export const COPY_PASSWORD_ID = "copy-password";
|
||||
export const COPY_USERNAME_ID = "copy-username";
|
||||
export const COPY_VERIFICATIONCODE_ID = "copy-totp";
|
||||
export const CREATE_CARD_ID = "create-card";
|
||||
export const CREATE_IDENTITY_ID = "create-identity";
|
||||
export const CREATE_LOGIN_ID = "create-login";
|
||||
export const GENERATE_PASSWORD_ID = "generate-password";
|
||||
export const NOOP_COMMAND_SUFFIX = "noop";
|
||||
export const ROOT_ID = "root";
|
||||
export const SEPARATOR_ID = "separator";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { UriMatchType } from "@bitwarden/common/enums";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
@ -55,5 +56,9 @@ export abstract class AutofillService {
|
||||
tab: chrome.tabs.Tab,
|
||||
fromCommand: boolean
|
||||
) => Promise<string | null>;
|
||||
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string | null>;
|
||||
doAutoFillActiveTab: (
|
||||
pageDetails: PageDetail[],
|
||||
fromCommand: boolean,
|
||||
cipherType?: CipherType
|
||||
) => Promise<string | null>;
|
||||
}
|
||||
|
@ -304,21 +304,47 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofill the active tab with the next login item from the cache
|
||||
* Autofill the active tab with the next cipher from the cache
|
||||
* @param {PageDetail[]} pageDetails The data scraped from the page
|
||||
* @param {boolean} fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
||||
* @returns {Promise<string | null>} The TOTP code of the successfully autofilled login, if any
|
||||
*/
|
||||
async doAutoFillActiveTab(
|
||||
pageDetails: PageDetail[],
|
||||
fromCommand: boolean
|
||||
fromCommand: boolean,
|
||||
cipherType?: CipherType
|
||||
): Promise<string | null> {
|
||||
const tab = await this.getActiveTab();
|
||||
|
||||
if (!tab || !tab.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
|
||||
if (!cipherType || cipherType === CipherType.Login) {
|
||||
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
|
||||
}
|
||||
|
||||
// Cipher is a non-login type
|
||||
const cipher: CipherView = (
|
||||
(await this.cipherService.getAllDecryptedForUrl(tab.url, [cipherType])) || []
|
||||
).find(({ type }) => type === cipherType);
|
||||
|
||||
if (!cipher || cipher.reprompt !== CipherRepromptType.None) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.doAutoFill({
|
||||
tab: tab,
|
||||
cipher: cipher,
|
||||
pageDetails: pageDetails,
|
||||
skipLastUsed: !fromCommand,
|
||||
skipUsernameOnlyFill: !fromCommand,
|
||||
onlyEmptyFields: !fromCommand,
|
||||
onlyVisibleFields: !fromCommand,
|
||||
fillNewPassword: false,
|
||||
allowUntrustedIframe: fromCommand,
|
||||
allowTotpAutofill: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
export type UserSettings = {
|
||||
avatarColor: string | null;
|
||||
@ -58,3 +59,5 @@ export type FillableFormFieldElement = HTMLInputElement | HTMLSelectElement | HT
|
||||
export type FormFieldElement = FillableFormFieldElement | HTMLSpanElement;
|
||||
|
||||
export type FormElementWithAttribute = FormFieldElement & Record<string, string | null | undefined>;
|
||||
|
||||
export type AutofillCipherTypeId = CipherType.Login | CipherType.Card | CipherType.Identity;
|
||||
|
@ -6,6 +6,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
@ -123,12 +124,28 @@ export default class RuntimeBackground {
|
||||
}
|
||||
break;
|
||||
case "openAddEditCipher": {
|
||||
const addEditCipherUrl =
|
||||
cipherId == null
|
||||
? "popup/index.html#/edit-cipher"
|
||||
: "popup/index.html#/edit-cipher?cipherId=" + cipherId;
|
||||
const isNewCipher = !cipherId;
|
||||
const cipherType = msg.data?.cipherType;
|
||||
const senderTab = sender.tab;
|
||||
|
||||
if (!senderTab) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isNewCipher) {
|
||||
await this.browserPopoutWindowService.openCipherCreation(senderTab.windowId, {
|
||||
cipherType,
|
||||
senderTabId: senderTab.id,
|
||||
senderTabURI: senderTab.url,
|
||||
});
|
||||
} else {
|
||||
await this.browserPopoutWindowService.openCipherEdit(senderTab.windowId, {
|
||||
cipherId,
|
||||
senderTabId: senderTab.id,
|
||||
senderTabURI: senderTab.url,
|
||||
});
|
||||
}
|
||||
|
||||
BrowserApi.openBitwardenExtensionTab(addEditCipherUrl, true);
|
||||
break;
|
||||
}
|
||||
case "closeTab":
|
||||
@ -174,6 +191,34 @@ export default class RuntimeBackground {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "autofill_card": {
|
||||
await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
frameId: sender.frameId,
|
||||
tab: msg.tab,
|
||||
details: msg.details,
|
||||
},
|
||||
],
|
||||
false,
|
||||
CipherType.Card
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "autofill_identity": {
|
||||
await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
frameId: sender.frameId,
|
||||
tab: msg.tab,
|
||||
details: msg.details,
|
||||
},
|
||||
],
|
||||
false,
|
||||
CipherType.Identity
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "contextMenu":
|
||||
clearTimeout(this.autofillTimeout);
|
||||
this.pageDetailsToAutoFill.push({
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
interface BrowserPopoutWindowService {
|
||||
openUnlockPrompt(senderWindowId: number): Promise<void>;
|
||||
closeUnlockPrompt(): Promise<void>;
|
||||
@ -9,6 +11,22 @@ interface BrowserPopoutWindowService {
|
||||
senderTabId: number;
|
||||
}
|
||||
): Promise<void>;
|
||||
openCipherCreation(
|
||||
senderWindowId: number,
|
||||
promptData: {
|
||||
cipherType?: CipherType;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
): Promise<void>;
|
||||
openCipherEdit(
|
||||
senderWindowId: number,
|
||||
promptData: {
|
||||
cipherId: string;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
): Promise<void>;
|
||||
closePasswordRepromptPrompt(): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import { BrowserPopoutWindowService as BrowserPopupWindowServiceInterface } from "./abstractions/browser-popout-window.service";
|
||||
@ -45,6 +47,50 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
|
||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "passwordReprompt");
|
||||
}
|
||||
|
||||
async openCipherCreation(
|
||||
senderWindowId: number,
|
||||
{
|
||||
cipherType = CipherType.Login,
|
||||
senderTabId,
|
||||
senderTabURI,
|
||||
}: {
|
||||
cipherType?: CipherType;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
) {
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/edit-cipher" +
|
||||
"?uilocation=popout" +
|
||||
`&type=${cipherType}` +
|
||||
`&senderTabId=${senderTabId}` +
|
||||
`&uri=${senderTabURI}`;
|
||||
|
||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "cipherCreation");
|
||||
}
|
||||
|
||||
async openCipherEdit(
|
||||
senderWindowId: number,
|
||||
{
|
||||
cipherId,
|
||||
senderTabId,
|
||||
senderTabURI,
|
||||
}: {
|
||||
cipherId: string;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
) {
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/edit-cipher" +
|
||||
"?uilocation=popout" +
|
||||
`&cipherId=${cipherId}` +
|
||||
`&senderTabId=${senderTabId}` +
|
||||
`&uri=${senderTabURI}`;
|
||||
|
||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "cipherEdit");
|
||||
}
|
||||
|
||||
async closePasswordRepromptPrompt() {
|
||||
await this.closeSingleActionPopout("passwordReprompt");
|
||||
}
|
||||
|
@ -35,6 +35,9 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
showAttachments = true;
|
||||
openAttachmentsInPopup: boolean;
|
||||
showAutoFillOnPageLoadOptions: boolean;
|
||||
senderTabId?: number;
|
||||
uilocation?: "popout" | "popup" | "sidebar" | "tab";
|
||||
inPopout = false;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@ -81,6 +84,9 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.senderTabId = parseInt(params?.senderTabId, 10) || undefined;
|
||||
this.uilocation = params?.uilocation;
|
||||
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
@ -128,6 +134,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
this.openAttachmentsInPopup = this.popupUtilsService.inPopup(window);
|
||||
});
|
||||
|
||||
this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window);
|
||||
|
||||
if (!this.editMode) {
|
||||
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
||||
this.currentUris =
|
||||
@ -162,6 +170,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.senderTabId && this.inPopout) {
|
||||
setTimeout(() => this.close(), 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.cloneMode) {
|
||||
this.router.navigate(["/tabs/vault"]);
|
||||
} else {
|
||||
@ -194,6 +207,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
cancel() {
|
||||
super.cancel();
|
||||
|
||||
if (this.senderTabId && this.inPopout) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.popupUtilsService.inTab(window)) {
|
||||
this.messagingService.send("closeTab");
|
||||
return;
|
||||
@ -202,6 +220,14 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
// Used for closing single-action views
|
||||
close() {
|
||||
BrowserApi.focusTab(this.senderTabId);
|
||||
window.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<boolean> {
|
||||
const confirmed = await super.generateUsername();
|
||||
if (confirmed) {
|
||||
|
Loading…
Reference in New Issue
Block a user