diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7b6d24aa8c0..763b48ab1d9 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,6 +4,11 @@
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+## Desktop native module ##
+apps/desktop/desktop_native @bitwarden/team-platform-dev
+apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
+apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
+
## Auth team files ##
apps/browser/src/auth @bitwarden/team-auth-dev
apps/cli/src/auth @bitwarden/team-auth-dev
@@ -124,10 +129,6 @@ apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation
apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation
apps/web/src/app/layouts @bitwarden/team-ui-foundation
-## Desktop native module ##
-apps/desktop/desktop_native @bitwarden/team-platform-dev
-apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
-apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
## Key management team files ##
apps/desktop/src/key-management @bitwarden/team-key-management-dev
diff --git a/.github/renovate.json b/.github/renovate.json5
similarity index 58%
rename from .github/renovate.json
rename to .github/renovate.json5
index f1efcbaffbe..6d6fbbd2539 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json5
@@ -1,46 +1,46 @@
{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": ["github>bitwarden/renovate-config"],
- "enabledManagers": ["cargo", "github-actions", "npm"],
- "packageRules": [
+ $schema: "https://docs.renovatebot.com/renovate-schema.json",
+ extends: ["github>bitwarden/renovate-config"], // Extends our base config for pinned dependencies
+ enabledManagers: ["cargo", "github-actions", "npm"],
+ packageRules: [
{
- "groupName": "github-action minor",
- "matchManagers": ["github-actions"],
- "matchUpdateTypes": ["minor"]
+ groupName: "github-action minor",
+ matchManagers: ["github-actions"],
+ matchUpdateTypes: ["minor"],
},
{
- "matchManagers": ["cargo"],
- "commitMessagePrefix": "[deps] Platform:"
+ matchManagers: ["cargo"],
+ commitMessagePrefix: "[deps] Platform:",
},
{
- "groupName": "napi",
- "matchPackageNames": ["napi", "napi-build", "napi-derive"]
+ groupName: "napi",
+ matchPackageNames: ["napi", "napi-build", "napi-derive"],
},
{
- "matchPackageNames": ["typescript", "zone.js"],
- "matchUpdateTypes": ["major", "minor"],
- "description": "Determined by Angular",
- "enabled": false
+ matchPackageNames: ["typescript", "zone.js"],
+ matchUpdateTypes: ["major", "minor"],
+ description: "Determined by Angular",
+ enabled: false,
},
{
- "matchPackageNames": ["typescript", "zone.js"],
- "matchUpdateTypes": "patch"
+ matchPackageNames: ["typescript", "zone.js"],
+ matchUpdateTypes: "patch",
},
{
- "groupName": "jest",
- "matchPackageNames": ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
- "matchUpdateTypes": "major"
+ groupName: "jest",
+ matchPackageNames: ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
+ matchUpdateTypes: "major",
},
{
- "groupName": "macOS/iOS bindings",
- "matchPackageNames": ["core-foundation", "security-framework", "security-framework-sys"]
+ groupName: "macOS/iOS bindings",
+ matchPackageNames: ["core-foundation", "security-framework", "security-framework-sys"],
},
{
- "groupName": "zbus",
- "matchPackageNames": ["zbus", "zbus_polkit"]
+ groupName: "zbus",
+ matchPackageNames: ["zbus", "zbus_polkit"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"base64-loader",
"buffer",
"bufferutil",
@@ -56,20 +56,20 @@
"style-loader",
"ts-loader",
"url",
- "util"
+ "util",
],
- "description": "Admin Console owned dependencies",
- "commitMessagePrefix": "[deps] AC:",
- "reviewers": ["team:team-admin-console-dev"]
+ description: "Admin Console owned dependencies",
+ commitMessagePrefix: "[deps] AC:",
+ reviewers: ["team:team-admin-console-dev"],
},
{
- "matchPackageNames": ["qrious"],
- "description": "Auth owned dependencies",
- "commitMessagePrefix": "[deps] Auth:",
- "reviewers": ["team:team-auth-dev"]
+ matchPackageNames: ["qrious"],
+ description: "Auth owned dependencies",
+ commitMessagePrefix: "[deps] Auth:",
+ reviewers: ["team:team-auth-dev"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@angular-eslint/schematics",
"angular-eslint",
"eslint-config-prettier",
@@ -82,14 +82,14 @@
"eslint",
"husky",
"lint-staged",
- "typescript-eslint"
+ "typescript-eslint",
],
- "description": "Architecture owned dependencies",
- "commitMessagePrefix": "[deps] Architecture:",
- "reviewers": ["team:dept-architecture"]
+ description: "Architecture owned dependencies",
+ commitMessagePrefix: "[deps] Architecture:",
+ reviewers: ["team:dept-architecture"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@angular-eslint/eslint-plugin-template",
"@angular-eslint/eslint-plugin",
"@angular-eslint/schematics",
@@ -105,13 +105,13 @@
"eslint-plugin-tailwindcss",
"eslint",
"husky",
- "lint-staged"
+ "lint-staged",
],
- "groupName": "Linting minor-patch",
- "matchUpdateTypes": ["minor", "patch"]
+ groupName: "Linting minor-patch",
+ matchUpdateTypes: ["minor", "patch"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@emotion/css",
"@webcomponents/custom-elements",
"concurrently",
@@ -126,20 +126,20 @@
"@storybook/web-components-webpack5",
"tabbable",
"tldts",
- "wait-on"
+ "wait-on",
],
- "description": "Autofill owned dependencies",
- "commitMessagePrefix": "[deps] Autofill:",
- "reviewers": ["team:team-autofill-dev"]
+ description: "Autofill owned dependencies",
+ commitMessagePrefix: "[deps] Autofill:",
+ reviewers: ["team:team-autofill-dev"],
},
{
- "matchPackageNames": ["braintree-web-drop-in"],
- "description": "Billing owned dependencies",
- "commitMessagePrefix": "[deps] Billing:",
- "reviewers": ["team:team-billing-dev"]
+ matchPackageNames: ["braintree-web-drop-in"],
+ description: "Billing owned dependencies",
+ commitMessagePrefix: "[deps] Billing:",
+ reviewers: ["team:team-billing-dev"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@babel/core",
"@babel/preset-env",
"@bitwarden/sdk-internal",
@@ -167,6 +167,7 @@
"electron-updater",
"html-webpack-injector",
"html-webpack-plugin",
+ "json5",
"lowdb",
"node-forge",
"node-ipc",
@@ -179,14 +180,14 @@
"webpack",
"webpack-cli",
"webpack-dev-server",
- "webpack-node-externals"
+ "webpack-node-externals",
],
- "description": "Platform owned dependencies",
- "commitMessagePrefix": "[deps] Platform:",
- "reviewers": ["team:team-platform-dev"]
+ description: "Platform owned dependencies",
+ commitMessagePrefix: "[deps] Platform:",
+ reviewers: ["team:team-platform-dev"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@angular-devkit/build-angular",
"@angular/animations",
"@angular/cdk",
@@ -208,6 +209,7 @@
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-links",
+ "@storybook/addon-themes",
"@storybook/angular",
"@storybook/manager-api",
"@storybook/theming",
@@ -225,27 +227,27 @@
"remark-gfm",
"storybook",
"tailwindcss",
- "zone.js"
+ "zone.js",
],
- "description": "UI Foundation owned dependencies",
- "commitMessagePrefix": "[deps] UI Foundation:",
- "reviewers": ["team:team-ui-foundation"]
+ description: "UI Foundation owned dependencies",
+ commitMessagePrefix: "[deps] UI Foundation:",
+ reviewers: ["team:team-ui-foundation"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@types/jest",
"jest-junit",
"jest-mock-extended",
"jest-preset-angular",
"jest-diff",
- "ts-jest"
+ "ts-jest",
],
- "description": "Secrets Manager owned dependencies",
- "commitMessagePrefix": "[deps] SM:",
- "reviewers": ["team:team-secrets-manager-dev"]
+ description: "Secrets Manager owned dependencies",
+ commitMessagePrefix: "[deps] SM:",
+ reviewers: ["team:team-secrets-manager-dev"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@microsoft/signalr-protocol-msgpack",
"@microsoft/signalr",
"@types/jsdom",
@@ -256,14 +258,14 @@
"oidc-client-ts",
"papaparse",
"utf-8-validate",
- "zxcvbn"
+ "zxcvbn",
],
- "description": "Tools owned dependencies",
- "commitMessagePrefix": "[deps] Tools:",
- "reviewers": ["team:team-tools-dev"]
+ description: "Tools owned dependencies",
+ commitMessagePrefix: "[deps] Tools:",
+ reviewers: ["team:team-tools-dev"],
},
{
- "matchPackageNames": [
+ matchPackageNames: [
"@koa/multer",
"@koa/router",
"@types/inquirer",
@@ -289,18 +291,18 @@
"node-fetch",
"open",
"proper-lockfile",
- "qrcode-parser"
+ "qrcode-parser",
],
- "description": "Vault owned dependencies",
- "commitMessagePrefix": "[deps] Vault:",
- "reviewers": ["team:team-vault-dev"]
+ description: "Vault owned dependencies",
+ commitMessagePrefix: "[deps] Vault:",
+ reviewers: ["team:team-vault-dev"],
},
{
- "matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
- "description": "Key Management owned dependencies",
- "commitMessagePrefix": "[deps] KM:",
- "reviewers": ["team:team-key-management-dev"]
- }
+ matchPackageNames: ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
+ description: "Key Management owned dependencies",
+ commitMessagePrefix: "[deps] KM:",
+ reviewers: ["team:team-key-management-dev"],
+ },
],
- "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"]
+ ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"],
}
diff --git a/.storybook/main.ts b/.storybook/main.ts
index d98ca06ead3..9583d1fc6f2 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -29,6 +29,7 @@ const config: StorybookConfig = {
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-designs"),
getAbsolutePath("@storybook/addon-interactions"),
+ getAbsolutePath("@storybook/addon-themes"),
{
// @storybook/addon-docs is part of @storybook/addon-essentials
// eslint-disable-next-line storybook/no-uninstalled-addons
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 85515068b3a..6bd28cfe809 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,60 +1,30 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
+import { withThemeByClassName } from "@storybook/addon-themes";
import { componentWrapperDecorator } from "@storybook/angular";
import type { Preview } from "@storybook/angular";
import docJson from "../documentation.json";
setCompodocJson(docJson);
-const decorator = componentWrapperDecorator(
- (story) => {
- return /*html*/ `
-
- ${story}
-
+const wrapperDecorator = componentWrapperDecorator((story) => {
+ return /*html*/ `
+
+ ${story}
+
`;
- },
- ({ globals }) => {
- // We need to add the theme class to the body to support body-appended content like popovers and menus
- document.body.classList.remove("theme_light");
- document.body.classList.remove("theme_dark");
-
- document.body.classList.add(`theme_${globals["theme"]}`);
-
- return { theme: `${globals["theme"]}` };
- },
-);
+});
const preview: Preview = {
- decorators: [decorator],
- globalTypes: {
- theme: {
- description: "Global theme for components",
- defaultValue: "light",
- toolbar: {
- title: "Theme",
- icon: "circlehollow",
- items: [
- {
- title: "Light",
- value: "light",
- icon: "sun",
- },
- {
- title: "Dark",
- value: "dark",
- icon: "moon",
- },
- ],
- dynamicTitle: true,
+ decorators: [
+ withThemeByClassName({
+ themes: {
+ light: "theme_light",
+ dark: "theme_dark",
},
- },
- },
+ defaultTheme: "light",
+ }),
+ wrapperDecorator,
+ ],
parameters: {
controls: {
matchers: {
@@ -69,6 +39,9 @@ const preview: Preview = {
},
},
docs: { source: { type: "dynamic", excludeDecorators: true } },
+ backgrounds: {
+ disable: true,
+ },
},
tags: ["autodocs"],
};
diff --git a/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts
index 7ded23116ee..180a4685332 100644
--- a/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts
+++ b/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts
@@ -1,5 +1,6 @@
type InitContextMenuItems = Omit & {
- checkPremiumAccess?: boolean;
+ requiresPremiumAccess?: boolean;
+ requiresUnblockedUri?: boolean;
};
export { InitContextMenuItems };
diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts
index 038f4e85c9a..e2bf75350a2 100644
--- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts
+++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts
@@ -21,7 +21,7 @@ export class CipherContextMenuHandler {
private accountService: AccountService,
) {}
- async update(url: string) {
+ async update(url: string, currentUriIsBlocked: boolean = false) {
if (this.mainContextMenuHandler.initRunning) {
return;
}
@@ -88,6 +88,10 @@ export class CipherContextMenuHandler {
for (const cipher of ciphers) {
await this.updateForCipher(cipher);
}
+
+ if (currentUriIsBlocked) {
+ await this.mainContextMenuHandler.removeBlockedUriMenuItems();
+ }
}
private async updateForCipher(cipher: CipherView) {
diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts
index 79998b65205..267a832a671 100644
--- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts
+++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts
@@ -2,7 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants";
+import {
+ AUTOFILL_CARD_ID,
+ AUTOFILL_ID,
+ AUTOFILL_IDENTITY_ID,
+ COPY_IDENTIFIER_ID,
+ COPY_PASSWORD_ID,
+ COPY_USERNAME_ID,
+ COPY_VERIFICATION_CODE_ID,
+ NOOP_COMMAND_SUFFIX,
+ SEPARATOR_ID,
+} from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -15,6 +25,43 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { MainContextMenuHandler } from "./main-context-menu-handler";
+/**
+ * Used in place of Set method `symmetricDifference`, which is only available to node version 22.0.0 or greater:
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
+ */
+function symmetricDifference(setA: Set, setB: Set) {
+ const _difference = new Set(setA);
+ for (const elem of setB) {
+ if (_difference.has(elem)) {
+ _difference.delete(elem);
+ } else {
+ _difference.add(elem);
+ }
+ }
+ return _difference;
+}
+
+const createCipher = (data?: {
+ id?: CipherView["id"];
+ username?: CipherView["login"]["username"];
+ password?: CipherView["login"]["password"];
+ totp?: CipherView["login"]["totp"];
+ viewPassword?: CipherView["viewPassword"];
+}): CipherView => {
+ const { id, username, password, totp, viewPassword } = data || {};
+ const cipherView = new CipherView(
+ new Cipher({
+ id: id ?? "1",
+ type: CipherType.Login,
+ viewPassword: viewPassword ?? true,
+ } as any),
+ );
+ cipherView.login.username = username ?? "USERNAME";
+ cipherView.login.password = password ?? "PASSWORD";
+ cipherView.login.totp = totp ?? "TOTP";
+ return cipherView;
+};
+
describe("context-menu", () => {
let stateService: MockProxy;
let autofillSettingsService: MockProxy;
@@ -59,6 +106,9 @@ describe("context-menu", () => {
billingAccountProfileStateService,
accountService,
);
+
+ jest.spyOn(MainContextMenuHandler, "remove");
+
autofillSettingsService.enableContextMenu$ = of(true);
accountService.activeAccount$ = of({
id: "userId" as UserId,
@@ -68,7 +118,10 @@ describe("context-menu", () => {
});
});
- afterEach(() => jest.resetAllMocks());
+ afterEach(async () => {
+ await MainContextMenuHandler.removeAll();
+ jest.resetAllMocks();
+ });
describe("init", () => {
it("has menu disabled", async () => {
@@ -97,27 +150,6 @@ describe("context-menu", () => {
});
describe("loadOptions", () => {
- const createCipher = (data?: {
- id?: CipherView["id"];
- username?: CipherView["login"]["username"];
- password?: CipherView["login"]["password"];
- totp?: CipherView["login"]["totp"];
- viewPassword?: CipherView["viewPassword"];
- }): CipherView => {
- const { id, username, password, totp, viewPassword } = data || {};
- const cipherView = new CipherView(
- new Cipher({
- id: id ?? "1",
- type: CipherType.Login,
- viewPassword: viewPassword ?? true,
- } as any),
- );
- cipherView.login.username = username ?? "USERNAME";
- cipherView.login.password = password ?? "PASSWORD";
- cipherView.login.totp = totp ?? "TOTP";
- return cipherView;
- };
-
it("is not a login cipher", async () => {
await sut.loadOptions("TEST_TITLE", "1", {
...createCipher(),
@@ -128,33 +160,124 @@ describe("context-menu", () => {
});
it("creates item for autofill", async () => {
- await sut.loadOptions(
- "TEST_TITLE",
- "1",
- createCipher({
- username: "",
- totp: "",
- viewPassword: false,
- }),
+ const cipher = createCipher({
+ username: "",
+ totp: "",
+ viewPassword: true,
+ });
+ const optionId = "1";
+ await sut.loadOptions("TEST_TITLE", optionId, cipher);
+
+ expect(createSpy).toHaveBeenCalledTimes(2);
+
+ expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(2);
+
+ const expectedMenuItems = new Set([
+ AUTOFILL_ID + `_${optionId}`,
+ COPY_PASSWORD_ID + `_${optionId}`,
+ ]);
+
+ // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
+ // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
+ const expectedReceivedDiff = symmetricDifference(
+ expectedMenuItems,
+ MainContextMenuHandler["existingMenuItems"],
);
- expect(createSpy).toHaveBeenCalledTimes(1);
+ expect(expectedReceivedDiff.size).toEqual(0);
});
it("create entry for each cipher piece", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
-
- await sut.loadOptions("TEST_TITLE", "1", createCipher());
+ const optionId = "arbitraryString";
+ await sut.loadOptions("TEST_TITLE", optionId, createCipher());
expect(createSpy).toHaveBeenCalledTimes(4);
+
+ expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4);
+
+ const expectedMenuItems = new Set([
+ AUTOFILL_ID + `_${optionId}`,
+ COPY_PASSWORD_ID + `_${optionId}`,
+ COPY_USERNAME_ID + `_${optionId}`,
+ COPY_VERIFICATION_CODE_ID + `_${optionId}`,
+ ]);
+
+ // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
+ // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
+ const expectedReceivedDiff = symmetricDifference(
+ expectedMenuItems,
+ MainContextMenuHandler["existingMenuItems"],
+ );
+
+ expect(expectedReceivedDiff.size).toEqual(0);
});
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
- await sut.loadOptions("TEST_TITLE", "NOOP");
+ const optionId = "NOOP";
+ await sut.loadOptions("TEST_TITLE", optionId);
expect(createSpy).toHaveBeenCalledTimes(6);
+
+ expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(6);
+
+ const expectedMenuItems = new Set([
+ AUTOFILL_ID + `_${optionId}`,
+ COPY_PASSWORD_ID + `_${optionId}`,
+ COPY_USERNAME_ID + `_${optionId}`,
+ COPY_VERIFICATION_CODE_ID + `_${optionId}`,
+ AUTOFILL_CARD_ID + `_${optionId}`,
+ AUTOFILL_IDENTITY_ID + `_${optionId}`,
+ ]);
+
+ // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
+ // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
+ const expectedReceivedDiff = symmetricDifference(
+ expectedMenuItems,
+ MainContextMenuHandler["existingMenuItems"],
+ );
+
+ expect(expectedReceivedDiff.size).toEqual(0);
+ });
+ });
+
+ describe("removeBlockedUriMenuItems", () => {
+ it("removes menu items that require code injection", async () => {
+ billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
+ autofillSettingsService.enableContextMenu$ = of(true);
+ stateService.getIsAuthenticated.mockResolvedValue(true);
+
+ const optionId = "1";
+ await sut.loadOptions("TEST_TITLE", optionId, createCipher());
+
+ await sut.removeBlockedUriMenuItems();
+
+ expect(MainContextMenuHandler["remove"]).toHaveBeenCalledTimes(5);
+ expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_ID);
+ expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_IDENTITY_ID);
+ expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_CARD_ID);
+ expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(SEPARATOR_ID + 2);
+ expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(COPY_IDENTIFIER_ID);
+
+ expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4);
+
+ const expectedMenuItems = new Set([
+ AUTOFILL_ID + `_${optionId}`,
+ COPY_PASSWORD_ID + `_${optionId}`,
+ COPY_USERNAME_ID + `_${optionId}`,
+ COPY_VERIFICATION_CODE_ID + `_${optionId}`,
+ ]);
+
+ // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
+ // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
+ const expectedReceivedDiff = symmetricDifference(
+ expectedMenuItems,
+ MainContextMenuHandler["existingMenuItems"],
+ );
+
+ expect(expectedReceivedDiff.size).toEqual(0);
});
});
diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts
index 41d88439e8f..ad9dc34e501 100644
--- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts
+++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts
@@ -31,6 +31,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
export class MainContextMenuHandler {
+ static existingMenuItems: Set = new Set();
initRunning = false;
private initContextMenuItems: InitContextMenuItems[] = [
{
@@ -41,6 +42,7 @@ export class MainContextMenuHandler {
id: AUTOFILL_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFillLogin"),
+ requiresUnblockedUri: true,
},
{
id: COPY_USERNAME_ID,
@@ -56,7 +58,7 @@ export class MainContextMenuHandler {
id: COPY_VERIFICATION_CODE_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyVerificationCode"),
- checkPremiumAccess: true,
+ requiresPremiumAccess: true,
},
{
id: SEPARATOR_ID + 1,
@@ -67,16 +69,19 @@ export class MainContextMenuHandler {
id: AUTOFILL_IDENTITY_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFillIdentity"),
+ requiresUnblockedUri: true,
},
{
id: AUTOFILL_CARD_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFillCard"),
+ requiresUnblockedUri: true,
},
{
id: SEPARATOR_ID + 2,
type: "separator",
parentId: ROOT_ID,
+ requiresUnblockedUri: true,
},
{
id: GENERATE_PASSWORD_ID,
@@ -87,6 +92,7 @@ export class MainContextMenuHandler {
id: COPY_IDENTIFIER_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyElementIdentifier"),
+ requiresUnblockedUri: true,
},
];
private noCardsContextMenuItems: chrome.contextMenus.CreateProperties[] = [
@@ -175,13 +181,19 @@ export class MainContextMenuHandler {
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
);
- for (const options of this.initContextMenuItems) {
- if (options.checkPremiumAccess && !hasPremium) {
+ for (const menuItem of this.initContextMenuItems) {
+ const {
+ requiresPremiumAccess,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ requiresUnblockedUri, // destructuring this out of being passed to `create`
+ ...otherOptions
+ } = menuItem;
+
+ if (requiresPremiumAccess && !hasPremium) {
continue;
}
- delete options.checkPremiumAccess;
- await MainContextMenuHandler.create({ ...options, contexts: ["all"] });
+ await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
}
} catch (error) {
this.logService.warning(error.message);
@@ -202,12 +214,16 @@ export class MainContextMenuHandler {
}
return new Promise((resolve, reject) => {
- chrome.contextMenus.create(options, () => {
+ const itemId = chrome.contextMenus.create(options, () => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve();
});
+
+ this.existingMenuItems.add(`${itemId}`);
+
+ return itemId;
});
};
@@ -221,12 +237,16 @@ export class MainContextMenuHandler {
resolve();
});
+
+ this.existingMenuItems = new Set();
+
+ return;
});
}
static remove(menuItemId: string) {
return new Promise((resolve, reject) => {
- chrome.contextMenus.remove(menuItemId, () => {
+ const itemId = chrome.contextMenus.remove(menuItemId, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
@@ -234,6 +254,10 @@ export class MainContextMenuHandler {
resolve();
});
+
+ this.existingMenuItems.delete(`${itemId}`);
+
+ return;
});
}
@@ -244,6 +268,11 @@ export class MainContextMenuHandler {
const createChildItem = async (parentId: string) => {
const menuItemId = `${parentId}_${optionId}`;
+ const itemAlreadyExists = MainContextMenuHandler.existingMenuItems.has(menuItemId);
+ if (itemAlreadyExists) {
+ return;
+ }
+
return await MainContextMenuHandler.create({
type: "normal",
id: menuItemId,
@@ -255,10 +284,18 @@ export class MainContextMenuHandler {
if (
!cipher ||
- (cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
+ (cipher.type === CipherType.Login &&
+ (!Utils.isNullOrEmpty(cipher.login?.username) ||
+ !Utils.isNullOrEmpty(cipher.login?.password) ||
+ !Utils.isNullOrEmpty(cipher.login?.totp)))
) {
await createChildItem(AUTOFILL_ID);
+ }
+ if (
+ !cipher ||
+ (cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
+ ) {
if (cipher?.viewPassword ?? true) {
await createChildItem(COPY_PASSWORD_ID);
}
@@ -305,10 +342,22 @@ export class MainContextMenuHandler {
}
}
+ async removeBlockedUriMenuItems() {
+ try {
+ for (const menuItem of this.initContextMenuItems) {
+ if (menuItem.requiresUnblockedUri && menuItem.id) {
+ await MainContextMenuHandler.remove(menuItem.id);
+ }
+ }
+ } catch (error) {
+ this.logService.warning(error.message);
+ }
+ }
+
async noCards() {
try {
- for (const option of this.noCardsContextMenuItems) {
- await MainContextMenuHandler.create(option);
+ for (const menuItem of this.noCardsContextMenuItems) {
+ await MainContextMenuHandler.create(menuItem);
}
} catch (error) {
this.logService.warning(error.message);
@@ -317,8 +366,8 @@ export class MainContextMenuHandler {
async noIdentities() {
try {
- for (const option of this.noIdentitiesContextMenuItems) {
- await MainContextMenuHandler.create(option);
+ for (const menuItem of this.noIdentitiesContextMenuItems) {
+ await MainContextMenuHandler.create(menuItem);
}
} catch (error) {
this.logService.warning(error.message);
@@ -327,8 +376,8 @@ export class MainContextMenuHandler {
async noLogins() {
try {
- for (const option of this.noLoginsContextMenuItems) {
- await MainContextMenuHandler.create(option);
+ for (const menuItem of this.noLoginsContextMenuItems) {
+ await MainContextMenuHandler.create(menuItem);
}
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts
index 992b034afa7..6cc56e079d4 100644
--- a/apps/browser/src/autofill/content/components/icons/index.ts
+++ b/apps/browser/src/autofill/content/components/icons/index.ts
@@ -10,3 +10,4 @@ export { PartyHorn } from "./party-horn";
export { PencilSquare } from "./pencil-square";
export { Shield } from "./shield";
export { User } from "./user";
+export { Warning } from "./warning";
diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/icons/party-horn.ts
index dc2144b524f..e807df1d86e 100644
--- a/apps/browser/src/autofill/content/components/icons/party-horn.ts
+++ b/apps/browser/src/autofill/content/components/icons/party-horn.ts
@@ -6,168 +6,262 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
export function PartyHorn({ theme }: { theme: Theme }) {
if (theme === ThemeTypes.Dark) {
return html`
-