diff --git a/.eslintrc.json b/.eslintrc.json
index c10904c8670..3fd6dec3d7e 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -56,6 +56,7 @@
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }],
+ "@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }],
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"no-console": "error",
"import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package.
diff --git a/.github/renovate.json b/.github/renovate.json
index 776c66af68e..150ac1ac99d 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -4,13 +4,9 @@
"enabledManagers": ["cargo", "github-actions", "npm"],
"packageRules": [
{
- "groupName": "gh minor",
+ "groupName": "github-action minor",
"matchManagers": ["github-actions"],
- "matchUpdateTypes": ["minor", "patch"]
- },
- {
- "matchManagers": ["github-actions"],
- "commitMessagePrefix": "[deps] BRE:"
+ "matchUpdateTypes": ["minor"]
},
{
"matchManagers": ["cargo"],
@@ -86,6 +82,7 @@
"prettier",
"prettier-plugin-tailwindcss",
"rimraf",
+ "@storybook/web-components-webpack5",
"tabbable",
"tldts",
"wait-on"
@@ -210,10 +207,10 @@
"eslint-plugin-storybook",
"eslint-plugin-tailwindcss",
"husky",
- "jest-extended",
"jest-junit",
"jest-mock-extended",
"jest-preset-angular",
+ "jest-diff",
"lint-staged",
"ts-jest"
],
diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml
index b0874b38cbf..a09e8137b65 100644
--- a/.github/workflows/scan.yml
+++ b/.github/workflows/scan.yml
@@ -66,10 +66,9 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
- uses: sonarsource/sonarcloud-github-action@02ef91109b2d589e757aefcfb2854c2783fd7b19 # v4.0.0
+ uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
diff --git a/apps/browser/package.json b/apps/browser/package.json
index cf9309728ae..9ad1805362e 100644
--- a/apps/browser/package.json
+++ b/apps/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
- "version": "2025.1.0",
+ "version": "2025.1.1",
"scripts": {
"build": "npm run build:chrome",
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack",
@@ -30,6 +30,7 @@
"dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari",
"test": "jest",
"test:watch": "jest --watch",
- "test:watch:all": "jest --watchAll"
+ "test:watch:all": "jest --watchAll",
+ "test:clearCache": "jest --clear-cache"
}
}
diff --git a/apps/browser/postcss.config.js b/apps/browser/postcss.config.js
index c4513687e89..83e237f06e5 100644
--- a/apps/browser/postcss.config.js
+++ b/apps/browser/postcss.config.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-undef */
+/* eslint-disable no-undef, @typescript-eslint/no-require-imports */
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
};
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index b72a909252b..51e1203673b 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -2339,11 +2339,11 @@
"blockedDomainsDesc": {
"message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
},
- "autofillBlockedNotice": {
- "message": "Autofill is blocked for this website. Review or change this in settings."
+ "autofillBlockedNoticeV2": {
+ "message": "Autofill is blocked for this website."
},
- "autofillBlockedTooltip": {
- "message": "Autofill is blocked on this website. Review in settings."
+ "autofillBlockedNoticeGuidance": {
+ "message": "Change this in settings"
},
"websiteItemLabel": {
"message": "Website $number$ (URI)",
@@ -4010,6 +4010,9 @@
"autofillSuggestions": {
"message": "Autofill suggestions"
},
+ "itemSuggestions": {
+ "message": "Suggested items"
+ },
"autofillSuggestionsTip": {
"message": "Save a login item for this site to autofill"
},
@@ -4586,12 +4589,6 @@
"textSends": {
"message": "Text Sends"
},
- "bitwardenNewLook": {
- "message": "Bitwarden has a new look!"
- },
- "bitwardenNewLookDesc": {
- "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!"
- },
"accountActions": {
"message": "Account actions"
},
diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html
index 0f2754b2bf2..8bc28c9754d 100644
--- a/apps/browser/src/auth/popup/settings/account-security.component.html
+++ b/apps/browser/src/auth/popup/settings/account-security.component.html
@@ -20,7 +20,7 @@
{{ "unlockMethods" | i18n }}
{{ biometricUnavailabilityReason }}
-
+
{
try {
- const result = await this.biometricsService.authenticateWithBiometrics();
+ const userId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a.id)),
+ );
+ let result = false;
+ try {
+ const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId);
+ result = await this.keyService.validateUserKey(userKey, userId);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (e) {
+ result = false;
+ }
// prevent duplicate dialog
biometricsResponseReceived = true;
diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts
index 03284f3fd89..6ad9b8e06fd 100644
--- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts
+++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts
@@ -57,6 +57,17 @@ export type InlineMenuElementPosition = {
height: number;
};
+export type FieldRect = {
+ bottom: number;
+ height: number;
+ left: number;
+ right: number;
+ top: number;
+ width: number;
+ x: number;
+ y: number;
+};
+
export type InlineMenuPosition = {
button?: InlineMenuElementPosition;
list?: InlineMenuElementPosition;
@@ -134,6 +145,7 @@ export type OverlayBackgroundExtensionMessage = {
isFieldCurrentlyFilling?: boolean;
subFrameData?: SubFrameOffsetData;
focusedFieldData?: FocusedFieldData;
+ allFieldsRect?: any;
isOpeningFullInlineMenu?: boolean;
styles?: Partial;
data?: LockedVaultPendingNotificationsData;
diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts
index 73f936bb591..a300ac08660 100644
--- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts
+++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts
@@ -453,12 +453,16 @@ describe("AutoSubmitLoginBackground", () => {
sendMockExtensionMessage({ command: "triggerAutoSubmitLogin" }, sender);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled;
});
it("skips acting on messages whose command does not have a registered handler", () => {
sendMockExtensionMessage({ command: "someInvalidCommand" }, sender);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled;
});
diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts
index 0ac69317855..c3a6357ed05 100644
--- a/apps/browser/src/autofill/background/overlay.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay.background.spec.ts
@@ -2913,6 +2913,124 @@ describe("OverlayBackground", () => {
);
});
});
+ describe("handles menu position when input is focused", () => {
+ it("sets button and menu width and position when non-multi-input totp field is focused", async () => {
+ const subframe = {
+ top: 0,
+ left: 0,
+ url: "",
+ frameId: 0,
+ };
+
+ overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
+ focusedFieldRects: {
+ width: 49.328125,
+ height: 64,
+ top: 302.171875,
+ left: 1270.8125,
+ },
+ });
+
+ const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe);
+ const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe);
+
+ expect(menuPostion).toEqual({
+ width: "49px",
+ top: "366px",
+ left: "1271px",
+ });
+ expect(buttonPostion).toEqual({
+ width: "34px",
+ height: "34px",
+ top: "317px",
+ left: "1271px",
+ });
+ });
+ it("sets button and menu width and position when multi-input totp field is focused", async () => {
+ const subframe = {
+ top: 0,
+ left: 0,
+ url: "",
+ frameId: 0,
+ };
+
+ const totpFields = [
+ createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__0" }),
+ createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__1" }),
+ createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__2" }),
+ ];
+ const allFieldData = [
+ createAutofillFieldMock({
+ autoCompleteType: "one-time-code",
+ opid: "__0",
+ rect: {
+ x: 1041.5,
+ y: 302.171875,
+ width: 49.328125,
+ height: 64,
+ top: 302.171875,
+ right: 1090.828125,
+ bottom: 366.171875,
+ left: 1041.5,
+ },
+ }),
+ createAutofillFieldMock({
+ autoCompleteType: "one-time-code",
+ opid: "__1",
+ rect: {
+ x: 1098.828125,
+ y: 302.171875,
+ width: 49.328125,
+ height: 64,
+ top: 302.171875,
+ right: 1148.15625,
+ bottom: 366.171875,
+ left: 1098.828125,
+ },
+ }),
+ createAutofillFieldMock({
+ autoCompleteType: "one-time-code",
+ opid: "__2",
+ rect: {
+ x: 1156.15625,
+ y: 302.171875,
+ width: 249.328125,
+ height: 64,
+ top: 302.171875,
+ right: 2205.484375,
+ bottom: 366.171875,
+ left: 2156.15625,
+ },
+ }),
+ ];
+ overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
+ focusedFieldRects: {
+ width: 49.328125,
+ height: 64,
+ top: 302.171875,
+ left: 1270.8125,
+ },
+ });
+
+ overlayBackground["allFieldData"] = allFieldData;
+ jest.spyOn(overlayBackground as any, "isTotpFieldForCurrentField").mockReturnValue(true);
+ jest.spyOn(overlayBackground as any, "getTotpFields").mockReturnValue(totpFields);
+
+ const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe);
+ const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe);
+ expect(menuPostion).toEqual({
+ width: "1164px",
+ top: "366px",
+ left: "1042px",
+ });
+ expect(buttonPostion).toEqual({
+ width: "34px",
+ height: "34px",
+ top: "292px",
+ left: "2187px",
+ });
+ });
+ });
describe("triggerDelayedAutofillInlineMenuClosure message handler", () => {
it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => {
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index 58e462943bf..3d2b1ec783c 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -70,6 +70,7 @@ import {
generateDomainMatchPatterns,
generateRandomChars,
isInvalidResponseStatusCode,
+ rectHasSize,
specialCharacterToKeyMap,
} from "../utils";
@@ -130,6 +131,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private currentInlineMenuCiphersCount: number = 0;
private currentAddNewItemData: CurrentAddNewItemData;
private focusedFieldData: FocusedFieldData;
+ private allFieldData: AutofillField[];
private isFieldCurrentlyFocused: boolean = false;
private isFieldCurrentlyFilling: boolean = false;
private isInlineMenuButtonVisible: boolean = false;
@@ -1367,6 +1369,71 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.isInlineMenuListVisible = false;
}
+ /**
+ * Get all the totp fields for the tab and frame of the currently focused field
+ */
+ private getTotpFields(): AutofillField[] {
+ const currentTabId = this.focusedFieldData?.tabId;
+ const currentFrameId = this.focusedFieldData?.frameId;
+ const pageDetailsMap = this.pageDetailsForTab[currentTabId];
+ const pageDetails = pageDetailsMap?.get(currentFrameId);
+
+ const fields = pageDetails.details.fields;
+ const totpFields = fields.filter((f) =>
+ this.inlineMenuFieldQualificationService.isTotpField(f),
+ );
+
+ return totpFields;
+ }
+
+ /**
+ * calculates the postion and width for multi-input totp field inline menu
+ * @param totpFieldArray - the totp fields used to evaluate the position of the menu
+ */
+ private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) {
+ // Filter the fields based on the provided totpfields
+ const filteredObjects = this.allFieldData.filter((obj) =>
+ totpFieldArray.some((o) => o.opid === obj.opid),
+ );
+
+ // Return null if no matching objects are found
+ if (filteredObjects.length === 0) {
+ return null;
+ }
+ // Calculate the smallest left and largest right values to determine width
+ const left = Math.min(
+ ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.left),
+ );
+ const largestRight = Math.max(
+ ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.right),
+ );
+
+ const width = largestRight - left;
+
+ return { left, width };
+ }
+
+ /**
+ * calculates the postion for multi-input totp field inline button
+ * @param totpFieldArray - the totp fields used to evaluate the position of the menu
+ */
+ private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) {
+ const filteredObjects = this.allFieldData.filter((obj) =>
+ totpFieldArray.some((o) => o.opid === obj.opid),
+ );
+
+ if (filteredObjects.length === 0) {
+ return null;
+ }
+
+ const maxRight = Math.max(...filteredObjects.map((obj) => obj.rect.right));
+ const maxObject = filteredObjects.find((obj) => obj.rect.right === maxRight);
+ const top = maxObject.rect.top - maxObject.rect.height * 0.39;
+ const left = maxRight - maxObject.rect.height * 0.3;
+
+ return { left, top };
+ }
+
/**
* Updates the position of either the inline menu list or button. The position
* is based on the focused field's position and dimensions.
@@ -1472,8 +1539,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const subFrameTopOffset = subFrameOffsets?.top || 0;
const subFrameLeftOffset = subFrameOffsets?.left || 0;
- const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
+ const { width, height } = this.focusedFieldData.focusedFieldRects;
+ let { top, left } = this.focusedFieldData.focusedFieldRects;
const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles;
+
+ if (this.isTotpFieldForCurrentField()) {
+ const totpFields = this.getTotpFields();
+ if (totpFields.length > 1) {
+ ({ left, top } = this.calculateTotpMultiInputButtonBounds(totpFields));
+ }
+ }
+
let elementOffset = height * 0.37;
if (height >= 35) {
elementOffset = height >= 50 ? height * 0.47 : height * 0.42;
@@ -1512,7 +1588,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const subFrameTopOffset = subFrameOffsets?.top || 0;
const subFrameLeftOffset = subFrameOffsets?.left || 0;
- const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
+ const { top, height } = this.focusedFieldData.focusedFieldRects;
+ let { left, width } = this.focusedFieldData.focusedFieldRects;
+
+ if (this.isTotpFieldForCurrentField()) {
+ const totpFields = this.getTotpFields();
+
+ if (totpFields.length > 1) {
+ ({ left, width } = this.calculateTotpMultiInputMenuBounds(totpFields));
+ }
+ }
this.inlineMenuPosition.list = {
top: Math.round(top + height + subFrameTopOffset),
@@ -1535,7 +1620,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param sender - The sender of the extension message
*/
private setFocusedFieldData(
- { focusedFieldData }: OverlayBackgroundExtensionMessage,
+ { focusedFieldData, allFieldsRect }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
if (
@@ -1552,6 +1637,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const previousFocusedFieldData = this.focusedFieldData;
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId };
+ this.allFieldData = allFieldsRect;
this.isFieldCurrentlyFocused = true;
if (this.shouldUpdatePasswordGeneratorMenuOnFieldFocus()) {
diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts
index ff1dbd4e945..d70fc1e7446 100644
--- a/apps/browser/src/autofill/content/auto-submit-login.spec.ts
+++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts
@@ -46,6 +46,8 @@ describe("AutoSubmitLogin content script", () => {
beforeEach(() => {
jest.useFakeTimers();
setupEnvironmentDefaults();
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./auto-submit-login");
});
diff --git a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts
new file mode 100644
index 00000000000..9e2da59d992
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts
@@ -0,0 +1,67 @@
+import { dirname, join } from "path";
+import path from "path";
+import type { StorybookConfig } from "@storybook/web-components-webpack5";
+import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
+import remarkGfm from "remark-gfm";
+
+const getAbsolutePath = (value: string): string =>
+ dirname(require.resolve(join(value, "package.json")));
+
+const config: StorybookConfig = {
+ stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"],
+ addons: [
+ getAbsolutePath("@storybook/addon-links"),
+ getAbsolutePath("@storybook/addon-essentials"),
+ getAbsolutePath("@storybook/addon-a11y"),
+ getAbsolutePath("@storybook/addon-designs"),
+ getAbsolutePath("@storybook/addon-interactions"),
+ {
+ name: "@storybook/addon-docs",
+ options: {
+ mdxPluginOptions: {
+ mdxCompileOptions: {
+ remarkPlugins: [remarkGfm],
+ },
+ },
+ },
+ },
+ ],
+ framework: {
+ name: getAbsolutePath("@storybook/web-components-webpack5"),
+ options: {
+ legacyRootApi: true,
+ },
+ },
+ core: {
+ disableTelemetry: true,
+ },
+ env: (existingConfig) => ({
+ ...existingConfig,
+ FLAGS: JSON.stringify({}),
+ }),
+ webpackFinal: async (config) => {
+ if (config.resolve) {
+ config.resolve.plugins = [
+ new TsconfigPathsPlugin({
+ configFile: path.resolve(__dirname, "../../../../../tsconfig.json"),
+ }),
+ ] as any;
+ }
+
+ if (config.module && config.module.rules) {
+ config.module.rules.push({
+ test: /\.(ts|tsx)$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: require.resolve("ts-loader"),
+ },
+ ],
+ });
+ }
+ return config;
+ },
+ docs: {},
+};
+
+export default config;
diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts
index 695cbfd3b9d..cacd2b59f0e 100644
--- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts
+++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts
@@ -23,6 +23,8 @@ export function EditButton({
title=${buttonText}
class=${editButtonStyles({ disabled, theme })}
@click=${(event: Event) => {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
!disabled && buttonAction(event);
}}
>
diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts
index 24f528c5246..acdee756570 100644
--- a/apps/browser/src/autofill/content/components/cipher/types.ts
+++ b/apps/browser/src/autofill/content/components/cipher/types.ts
@@ -1,3 +1,5 @@
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const CipherTypes = {
Login: 1,
SecureNote: 2,
@@ -7,6 +9,8 @@ const CipherTypes = {
type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes];
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const CipherRepromptTypes = {
None: 0,
Password: 1,
diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts
new file mode 100644
index 00000000000..aa53555d116
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts
@@ -0,0 +1,34 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { ActionButton } from "../../buttons/action-button";
+
+type Args = {
+ buttonText: string;
+ disabled: boolean;
+ theme: Theme;
+ buttonAction: (e: Event) => void;
+};
+
+export default {
+ title: "Components/Buttons/Action Button",
+ argTypes: {
+ buttonText: { control: "text" },
+ disabled: { control: "boolean" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ buttonAction: { control: false },
+ },
+ args: {
+ buttonText: "Click Me",
+ disabled: false,
+ theme: ThemeTypes.Light,
+ buttonAction: () => alert("Clicked"),
+ },
+} as Meta;
+
+const Template = (args: Args) => ActionButton({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts
new file mode 100644
index 00000000000..876a70eebc1
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts
@@ -0,0 +1,34 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { BadgeButton } from "../../buttons/badge-button";
+
+type Args = {
+ buttonAction: (e: Event) => void;
+ buttonText: string;
+ disabled?: boolean;
+ theme: Theme;
+};
+
+export default {
+ title: "Components/Buttons/Badge Button",
+ argTypes: {
+ buttonText: { control: "text" },
+ disabled: { control: "boolean" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ buttonAction: { control: false },
+ },
+ args: {
+ buttonText: "Click Me",
+ disabled: false,
+ theme: ThemeTypes.Light,
+ buttonAction: () => alert("Clicked"),
+ },
+} as Meta;
+
+const Template = (args: Args) => BadgeButton({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts
new file mode 100644
index 00000000000..dc202f330ae
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts
@@ -0,0 +1,29 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { CloseButton } from "../../buttons/close-button";
+
+type Args = {
+ handleCloseNotification: (e: Event) => void;
+ theme: Theme;
+};
+export default {
+ title: "Components/Buttons/Close Button",
+ argTypes: {
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ handleCloseNotification: { control: false },
+ },
+ args: {
+ theme: ThemeTypes.Light,
+ handleCloseNotification: () => {
+ alert("Close button clicked!");
+ },
+ },
+} as Meta;
+
+const Template = (args: Args) => CloseButton({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts
new file mode 100644
index 00000000000..769fe475dd5
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts
@@ -0,0 +1,33 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { EditButton } from "../../buttons/edit-button";
+
+type Args = {
+ buttonAction: (e: Event) => void;
+ buttonText: string;
+ disabled?: boolean;
+ theme: Theme;
+};
+export default {
+ title: "Components/Buttons/Edit Button",
+ argTypes: {
+ buttonText: { control: "text" },
+ disabled: { control: "boolean" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ buttonAction: { control: false },
+ },
+ args: {
+ buttonText: "Click Me",
+ disabled: false,
+ theme: ThemeTypes.Light,
+ buttonAction: () => alert("Clicked"),
+ },
+} as Meta;
+
+const Template = (args: Args) => EditButton({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts
new file mode 100644
index 00000000000..e597cddabe6
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts
@@ -0,0 +1,36 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
+import { CipherAction } from "../../cipher/cipher-action";
+
+type Args = {
+ handleAction?: (e: Event) => void;
+ notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add;
+ theme: Theme;
+};
+export default {
+ title: "Components/Ciphers/Cipher Action",
+ argTypes: {
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ notificationType: {
+ control: "select",
+ options: [NotificationTypes.Change, NotificationTypes.Add],
+ },
+ handleAction: { control: false },
+ },
+ args: {
+ theme: ThemeTypes.Light,
+ notificationType: NotificationTypes.Change,
+ handleAction: () => {
+ alert("Action triggered!");
+ },
+ },
+} as Meta;
+
+const Template = (args: Args) => CipherAction({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts
new file mode 100644
index 00000000000..a8884f063de
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts
@@ -0,0 +1,40 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+import { html } from "lit";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { CipherIcon } from "../../cipher/cipher-icon";
+
+type Args = {
+ color: string;
+ size: string;
+ theme: Theme;
+ uri?: string;
+};
+
+export default {
+ title: "Components/Ciphers/Cipher Icon",
+ argTypes: {
+ color: { control: "color" },
+ size: { control: "text" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ uri: { control: "text" },
+ },
+ args: {
+ size: "50px",
+ theme: ThemeTypes.Light,
+ uri: "",
+ },
+} as Meta;
+
+const Template = (args: Args) => {
+ return html`
+
+ ${CipherIcon({ ...args })}
+
+ `;
+};
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts
new file mode 100644
index 00000000000..2d031fa3afd
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts
@@ -0,0 +1,33 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+import { html } from "lit";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons";
+
+type Args = {
+ isBusinessOrg?: boolean;
+ isFamilyOrg?: boolean;
+ theme: Theme;
+};
+
+export default {
+ title: "Components/Ciphers/Cipher Indicator Icon",
+ argTypes: {
+ isBusinessOrg: { control: "boolean" },
+ isFamilyOrg: { control: "boolean" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ },
+ args: {
+ theme: ThemeTypes.Light,
+ isBusinessOrg: true,
+ isFamilyOrg: false,
+ },
+} as Meta;
+
+const Template: StoryObj["render"] = (args) =>
+ html`${CipherInfoIndicatorIcons({ ...args })}
`;
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts
new file mode 100644
index 00000000000..20c88a59246
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts
@@ -0,0 +1,66 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+import { html } from "lit";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import * as Icons from "../../icons";
+
+type Args = {
+ color?: string;
+ disabled?: boolean;
+ theme: Theme;
+ size: number;
+ iconLink: URL;
+};
+
+export default {
+ title: "Components/Icons/Icons",
+ argTypes: {
+ iconLink: { control: "text" },
+ color: { control: "color" },
+ disabled: { control: "boolean" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ size: { control: "number", min: 10, max: 100, step: 1 },
+ },
+ args: {
+ iconLink: new URL("https://bitwarden.com"),
+ disabled: false,
+ theme: ThemeTypes.Light,
+ size: 50,
+ },
+} as Meta;
+
+const Template = (args: Args, IconComponent: (props: Args) => ReturnType) => html`
+
+ ${IconComponent({ ...args })}
+
+`;
+
+const createIconStory = (iconName: keyof typeof Icons): StoryObj => {
+ const story = {
+ render: (args) => Template(args, Icons[iconName]),
+ } as StoryObj;
+
+ if (iconName !== "BrandIconContainer") {
+ story.argTypes = {
+ iconLink: { table: { disable: true } },
+ };
+ }
+
+ return story;
+};
+
+export const AngleDownIcon = createIconStory("AngleDown");
+export const BusinessIcon = createIconStory("Business");
+export const BrandIcon = createIconStory("BrandIconContainer");
+export const CloseIcon = createIconStory("Close");
+export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle");
+export const FamilyIcon = createIconStory("Family");
+export const FolderIcon = createIconStory("Folder");
+export const GlobeIcon = createIconStory("Globe");
+export const PartyHornIcon = createIconStory("PartyHorn");
+export const PencilSquareIcon = createIconStory("PencilSquare");
+export const ShieldIcon = createIconStory("Shield");
+export const UserIcon = createIconStory("User");
diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts
new file mode 100644
index 00000000000..00ea905a2f3
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts
@@ -0,0 +1,53 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+import { CipherType } from "@bitwarden/common/vault/enums";
+import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
+
+import { NotificationType } from "../../../../notification/abstractions/notification-bar";
+import { CipherData } from "../../cipher/types";
+import { NotificationBody } from "../../notification/body";
+
+type Args = {
+ ciphers: CipherData[];
+ notificationType: NotificationType;
+ theme: Theme;
+};
+
+export default {
+ title: "Components/Notifications/Notification Body",
+ argTypes: {
+ ciphers: { control: "object" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ notificationType: {
+ control: "select",
+ options: ["add", "change", "unlock", "fileless-import"],
+ },
+ },
+ args: {
+ ciphers: [
+ {
+ id: "1",
+ name: "Example Cipher",
+ type: CipherType.Login,
+ favorite: false,
+ reprompt: CipherRepromptType.None,
+ icon: {
+ imageEnabled: true,
+ image: "",
+ fallbackImage: "https://example.com/fallback.png",
+ icon: "icon-class",
+ },
+ login: { username: "user@example.com", passkey: null },
+ },
+ ],
+ theme: ThemeTypes.Light,
+ notificationType: "change",
+ },
+} as Meta;
+
+const Template = (args: Args) => NotificationBody({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts
new file mode 100644
index 00000000000..c8f30eb036f
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts
@@ -0,0 +1,32 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { NotificationType } from "../../../../notification/abstractions/notification-bar";
+import { NotificationFooter } from "../../notification/footer";
+
+type Args = {
+ notificationType: NotificationType;
+ theme: Theme;
+};
+
+export default {
+ title: "Components/Notifications/Notification Footer",
+ argTypes: {
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ notificationType: {
+ control: "select",
+ options: ["add", "change", "unlock", "fileless-import"],
+ },
+ },
+ args: {
+ theme: ThemeTypes.Light,
+ notificationType: "add",
+ },
+} as Meta;
+
+const Template = (args: Args) => NotificationFooter({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts
new file mode 100644
index 00000000000..fd8423f995e
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts
@@ -0,0 +1,33 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { NotificationHeader } from "../../notification/header";
+
+type Args = {
+ message: string;
+ standalone: boolean;
+ theme: Theme;
+ handleCloseNotification: (e: Event) => void;
+};
+
+export default {
+ title: "Components/Notifications/Notification Header",
+ argTypes: {
+ message: { control: "text" },
+ standalone: { control: "boolean" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ },
+ args: {
+ message: "This is a notification message",
+ standalone: true,
+ theme: ThemeTypes.Light,
+ handleCloseNotification: () => alert("Close Clicked"),
+ },
+} as Meta;
+
+const Template = (args: Args) => NotificationHeader({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts
new file mode 100644
index 00000000000..4b100764205
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts
@@ -0,0 +1,31 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { ActionRow } from "../../rows/action-row";
+
+type Args = {
+ itemText: string;
+ handleAction: (e: Event) => void;
+ theme: Theme;
+};
+
+export default {
+ title: "Components/Rows/Action Row",
+ argTypes: {
+ itemText: { control: "text" },
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ handleAction: { control: false },
+ },
+ args: {
+ itemText: "Action Item",
+ theme: ThemeTypes.Light,
+ handleAction: () => alert("Action triggered"),
+ },
+} as Meta;
+
+const Template = (args: Args) => ActionRow({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts
new file mode 100644
index 00000000000..3283c2798a3
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts
@@ -0,0 +1,25 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { ButtonRow } from "../../rows/button-row";
+
+type Args = {
+ theme: Theme;
+};
+
+export default {
+ title: "Components/Rows/Button Row",
+ argTypes: {
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ },
+ args: {
+ theme: ThemeTypes.Light,
+ },
+} as Meta;
+
+const Template = (args: Args) => ButtonRow({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts
new file mode 100644
index 00000000000..fbb65201986
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts
@@ -0,0 +1,28 @@
+import { Meta, StoryObj } from "@storybook/web-components";
+import { TemplateResult } from "lit";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
+
+import { ItemRow } from "../../rows/item-row";
+
+type Args = {
+ theme: Theme;
+ children: TemplateResult | TemplateResult[];
+};
+
+export default {
+ title: "Components/Rows/Item Row",
+ argTypes: {
+ theme: { control: "select", options: [...Object.values(ThemeTypes)] },
+ children: { control: "object" },
+ },
+ args: {
+ theme: ThemeTypes.Light,
+ },
+} as Meta;
+
+const Template = (args: Args) => ItemRow({ ...args });
+
+export const Default: StoryObj = {
+ render: Template,
+};
diff --git a/apps/browser/src/autofill/content/components/package.json b/apps/browser/src/autofill/content/components/package.json
new file mode 100644
index 00000000000..8dbe9e7f516
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@bitwarden/lit-components",
+ "version": "2025.1.1",
+ "scripts": {
+ "storybook:lit": "storybook dev -p 6006 -c ./.lit-storybook"
+ }
+}
diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts
index 226fcb4bd61..a37a2e07678 100644
--- a/apps/browser/src/autofill/content/content-message-handler.spec.ts
+++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts
@@ -19,6 +19,8 @@ describe("ContentMessageHandler", () => {
);
beforeEach(() => {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./content-message-handler");
});
diff --git a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts
index 1ad985bc8e9..317f63e756c 100644
--- a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts
+++ b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts
@@ -6,6 +6,8 @@ describe("TriggerAutofillScriptInjection", () => {
describe("init", () => {
it("sends a message to the extension background", () => {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("../content/trigger-autofill-script-injection");
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts
index fde98a58a5f..fd6a79733cb 100644
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts
+++ b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts
@@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum"
import AutofillOverlayButton from "./autofill-overlay-button.deprecated";
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./legacy-button.scss");
(function () {
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts
index 714ccbfbee5..5d587bd4293 100644
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts
+++ b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts
@@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum"
import AutofillOverlayList from "./autofill-overlay-list.deprecated";
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./legacy-list.scss");
(function () {
diff --git a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts
index 94bef354a79..8885ed6299c 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts
@@ -60,6 +60,8 @@ describe("Fido2 Content Script", () => {
chrome.runtime.connect = jest.fn(() => portSpy);
it("destroys the messenger when the port is disconnected", () => {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
triggerPortOnDisconnectEvent(portSpy);
@@ -75,6 +77,8 @@ describe("Fido2 Content Script", () => {
const mockResult = { credentialId: "mock" } as CreateCredentialResult;
jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
const response = await messenger.handler!(message, new AbortController());
@@ -99,6 +103,8 @@ describe("Fido2 Content Script", () => {
data: mock(),
});
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
await messenger.handler!(message, new AbortController());
@@ -121,6 +127,8 @@ describe("Fido2 Content Script", () => {
const abortController = new AbortController();
const abortSpy = jest.spyOn(abortController.signal, "removeEventListener");
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
await messenger.handler!(message, abortController);
@@ -141,6 +149,8 @@ describe("Fido2 Content Script", () => {
abortController.abort();
});
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
await messenger.handler!(message, abortController);
@@ -161,6 +171,8 @@ describe("Fido2 Content Script", () => {
const abortController = new AbortController();
jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage });
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
const result = messenger.handler!(message, abortController);
@@ -175,6 +187,8 @@ describe("Fido2 Content Script", () => {
contentType: "application/json",
}));
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled();
@@ -193,6 +207,8 @@ describe("Fido2 Content Script", () => {
},
}));
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled();
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts
index 6b9b41b5aac..69e17d26fe5 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts
@@ -22,6 +22,8 @@ describe("FIDO2 page-script for manifest v2", () => {
it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => {
Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true });
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-page-script-append.mv2");
expect(window.document.createElement).not.toHaveBeenCalled();
@@ -33,6 +35,8 @@ describe("FIDO2 page-script for manifest v2", () => {
return node;
});
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-page-script-append.mv2");
expect(window.document.createElement).toHaveBeenCalledWith("script");
@@ -48,6 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => {
return node;
});
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-page-script-append.mv2");
expect(window.document.createElement).toHaveBeenCalledWith("script");
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts
index fd033090cd4..4c1761c37ba 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts
@@ -267,6 +267,8 @@ import { Messenger } from "./messaging/messenger";
clearWaitForFocus();
void messenger.destroy();
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
/** empty */
}
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts
index 31e8c941e86..f1aec69193b 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts
@@ -55,6 +55,8 @@ describe("Fido2 page script with native WebAuthn support", () => {
setupMockedWebAuthnSupport();
beforeAll(() => {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-page-script");
});
@@ -166,6 +168,8 @@ describe("Fido2 page script with native WebAuthn support", () => {
contentType: "json/application",
}));
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
expect(Messenger.forDOMCommunication).not.toHaveBeenCalled();
@@ -184,6 +188,8 @@ describe("Fido2 page script with native WebAuthn support", () => {
},
}));
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-content-script");
expect(Messenger.forDOMCommunication).not.toHaveBeenCalled();
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts
index e354453ca59..af1838ec942 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts
@@ -50,6 +50,8 @@ describe("Fido2 page script without native WebAuthn support", () => {
const mockCreateCredentialsResult = createCreateCredentialResultMock();
const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
const mockCredentialAssertResult = createAssertCredentialResultMock();
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./fido2-page-script");
afterEach(() => {
diff --git a/apps/browser/src/autofill/models/autofill-field.ts b/apps/browser/src/autofill/models/autofill-field.ts
index 7660b4ce5f0..c0be60f1cd0 100644
--- a/apps/browser/src/autofill/models/autofill-field.ts
+++ b/apps/browser/src/autofill/models/autofill-field.ts
@@ -1,3 +1,4 @@
+import { FieldRect } from "../background/abstractions/overlay.background";
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
@@ -124,4 +125,9 @@ export default class AutofillField {
fieldQualifier?: AutofillFieldQualifierType;
accountCreationFieldType?: InlineMenuAccountCreationFieldTypes;
+
+ /**
+ * used for totp multiline calculations
+ */
+ fieldRect?: FieldRect;
}
diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts
index a4b8ae44b6a..2c0ebe8e8e7 100644
--- a/apps/browser/src/autofill/notification/bar.ts
+++ b/apps/browser/src/autofill/notification/bar.ts
@@ -15,6 +15,8 @@ import {
NotificationBarIframeInitData,
} from "./abstractions/notification-bar";
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./bar.scss");
const logService = new ConsoleLogService(false);
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts
index 0ed14a520c1..36ef3897c56 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts
@@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum"
import { AutofillInlineMenuButton } from "./autofill-inline-menu-button";
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./button.scss");
(function () {
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts
index c302c50b4a4..b46b208b084 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts
@@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum"
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./list.scss");
(function () {
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts
index 16d5c29d574..522b968e533 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts
@@ -1,3 +1,5 @@
+// FIXME: Remove when updating file. Eslint update
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./menu-container.scss");
import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container";
diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
index daa65d74ae6..acf0dedde27 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
@@ -957,8 +957,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
accountCreationFieldType: autofillFieldData?.accountCreationFieldType,
};
+ const allFields = this.formFieldElements;
+ const allFieldsRect = [];
+
+ for (const key of allFields.keys()) {
+ const rect = await this.getMostRecentlyFocusedFieldRects(key);
+ allFieldsRect.push({ ...allFields.get(key), rect }); // Add the combined result to the array
+ }
+
await this.sendExtensionMessage("updateFocusedFieldData", {
focusedFieldData: this.focusedFieldData,
+ allFieldsRect,
});
}
@@ -1408,6 +1417,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
url.origin + pathWithoutTrailingSlashAndSearch,
url.origin + pathWithoutTrailingSlashSearchAndHash,
]);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_error) {
return null;
}
diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts
index 0dbc246b235..16310397a03 100644
--- a/apps/browser/src/autofill/services/dom-query.service.ts
+++ b/apps/browser/src/autofill/services/dom-query.service.ts
@@ -235,6 +235,8 @@ export class DomQueryService implements DomQueryServiceInterface {
if ((chrome as any).dom?.openOrClosedShadowRoot) {
try {
return (chrome as any).dom.openOrClosedShadowRoot(node);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return null;
}
diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts
index 12d26914d82..0e102dcfd99 100644
--- a/apps/browser/src/autofill/utils/index.ts
+++ b/apps/browser/src/autofill/utils/index.ts
@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import { FieldRect } from "../background/abstractions/overlay.background";
import { AutofillPort } from "../enums/autofill-port.enum";
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
@@ -545,6 +546,17 @@ export const specialCharacterToKeyMap: Record = {
"/": "forwardSlashCharacterDescriptor",
};
+/**
+ * Determines if the current rect values are not all 0.
+ */
+export function rectHasSize(rect: FieldRect): boolean {
+ if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) {
+ return true;
+ }
+
+ return false;
+}
+
/**
* Checks if all the values corresponding to the specified keys in an object are null.
* If no keys are specified, checks all keys in the object.
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 4bec3d6cc0a..5d09122bbd6 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -632,11 +632,6 @@ export default class MainBackground {
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
- this.biometricsService = new BackgroundBrowserBiometricsService(
- runtimeNativeMessagingBackground,
- this.logService,
- );
-
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
this.pinService = new PinService(
@@ -665,6 +660,14 @@ export default class MainBackground {
this.kdfConfigService,
);
+ this.biometricsService = new BackgroundBrowserBiometricsService(
+ runtimeNativeMessagingBackground,
+ this.logService,
+ this.keyService,
+ this.biometricStateService,
+ this.messagingService,
+ );
+
this.appIdService = new AppIdService(this.storageService, this.logService);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts
index 116d048d2e8..b08f1c8b566 100644
--- a/apps/browser/src/background/nativeMessaging.background.ts
+++ b/apps/browser/src/background/nativeMessaging.background.ts
@@ -257,7 +257,7 @@ export class NativeMessagingBackground {
message.command == BiometricsCommands.Unlock ||
message.command == BiometricsCommands.IsAvailable
) {
- // TODO remove after 2025.01
+ // TODO remove after 2025.3
// wait until there is no other callbacks, or timeout
const call = await firstValueFrom(
race(
@@ -342,6 +342,8 @@ export class NativeMessagingBackground {
};
}
this.port.postMessage(msg);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.logService.info(
"[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.",
diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts
index 8e6fc562d14..3031134dc34 100644
--- a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts
+++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts
@@ -1,9 +1,18 @@
import { Injectable } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
-import { BiometricsService, BiometricsCommands, BiometricsStatus } from "@bitwarden/key-management";
+import {
+ BiometricsService,
+ BiometricsCommands,
+ BiometricsStatus,
+ KeyService,
+ BiometricStateService,
+} from "@bitwarden/key-management";
import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
import { BrowserApi } from "../../platform/browser/browser-api";
@@ -13,6 +22,9 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
constructor(
private nativeMessagingBackground: () => NativeMessagingBackground,
private logService: LogService,
+ private keyService: KeyService,
+ private biometricStateService: BiometricStateService,
+ private messagingService: MessagingService,
) {
super();
}
@@ -65,6 +77,8 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
}
}
return BiometricsStatus.Available;
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return BiometricsStatus.DesktopDisconnected;
}
@@ -74,12 +88,21 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
try {
await this.ensureConnected();
+ // todo remove after 2025.3
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.Unlock,
});
if (response.response == "unlocked") {
- return response.userKeyB64;
+ const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
+ const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
+ if (await this.keyService.validateUserKey(userKey, userId)) {
+ await this.biometricStateService.setBiometricUnlockEnabled(true);
+ await this.keyService.setUserKey(userKey, userId);
+ // to update badge and other things
+ this.messagingService.send("switchAccount", { userId });
+ return userKey;
+ }
} else {
return null;
}
@@ -89,7 +112,16 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
userId: userId,
});
if (response.response) {
- return response.userKeyB64;
+ // In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled
+ const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
+ const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
+ if (await this.keyService.validateUserKey(userKey, userId)) {
+ await this.biometricStateService.setBiometricUnlockEnabled(true);
+ await this.keyService.setUserKey(userKey, userId);
+ // to update badge and other things
+ this.messagingService.send("switchAccount", { userId });
+ return userKey;
+ }
} else {
return null;
}
@@ -98,6 +130,8 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
this.logService.info("Biometric unlock for user failed", e);
throw new Error("Biometric unlock failed");
}
+
+ return null;
}
async getBiometricsStatusForUser(id: UserId): Promise {
@@ -114,6 +148,8 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
userId: id,
})
).response;
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return BiometricsStatus.DesktopDisconnected;
}
diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts
index 0235ad5bd9c..d248a630cc6 100644
--- a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts
+++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts
@@ -29,13 +29,13 @@ export class ForegroundBrowserBiometricsService extends BiometricsService {
async unlockWithBiometricsForUser(userId: UserId): Promise {
const response = await BrowserApi.sendMessageWithResponse<{
- result: string;
+ result: UserKey;
error: string;
}>(BiometricsCommands.UnlockWithBiometricsForUser, { userId });
if (!response.result) {
return null;
}
- return SymmetricCryptoKey.fromString(response.result) as UserKey;
+ return SymmetricCryptoKey.fromString(response.result.keyB64) as UserKey;
}
async getBiometricsStatusForUser(id: UserId): Promise {
diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts
index 77c0fcaf50a..711ae4e378f 100644
--- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts
+++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts
@@ -54,7 +54,15 @@ export class ExtensionLockComponentService implements LockComponentService {
if (!(await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$))) {
return BiometricsStatus.NotEnabledLocally;
} else {
- return await this.biometricsService.getBiometricsStatusForUser(userId);
+ // TODO remove after 2025.3
+ // remove after backward compatibility code for old biometrics ipc protocol is removed
+ const result: BiometricsStatus = (await Promise.race([
+ this.biometricsService.getBiometricsStatusForUser(userId),
+ new Promise((resolve) =>
+ setTimeout(() => resolve(BiometricsStatus.DesktopDisconnected), 1000),
+ ),
+ ])) as BiometricsStatus;
+ return result;
}
}),
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json
index 9e4eb78291a..86ea0eebbc8 100644
--- a/apps/browser/src/manifest.json
+++ b/apps/browser/src/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
- "version": "2025.1.0",
+ "version": "2025.1.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json
index ef7ede6b056..ae7c888eb9b 100644
--- a/apps/browser/src/manifest.v3.json
+++ b/apps/browser/src/manifest.v3.json
@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
- "version": "2025.1.0",
+ "version": "2025.1.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts
index 383e982f065..2b1040bcd8a 100644
--- a/apps/browser/src/platform/flags.ts
+++ b/apps/browser/src/platform/flags.ts
@@ -11,13 +11,11 @@ import { GroupPolicyEnvironment } from "../admin-console/types/group-policy-envi
import { BrowserApi } from "./browser/browser-api";
// required to avoid linting errors when there are no flags
-// eslint-disable-next-line @typescript-eslint/ban-types
export type Flags = {
accountSwitching?: boolean;
} & SharedFlags;
// required to avoid linting errors when there are no flags
-// eslint-disable-next-line @typescript-eslint/ban-types
export type DevFlags = {
managedEnvironment?: GroupPolicyEnvironment;
} & SharedDevFlags;
diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts
index 4065f2e46d7..67fa920d18d 100644
--- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts
+++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts
@@ -8,6 +8,8 @@ describe("OffscreenDocument", () => {
const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read");
const consoleErrorSpy = jest.spyOn(console, "error");
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("../offscreen-document/offscreen-document");
describe("init", () => {
diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts
index 949ecebde8a..4d6a403a18a 100644
--- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts
+++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts
@@ -48,6 +48,8 @@ describe("LocalBackedSessionStorage", () => {
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test");
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
encrypted,
sessionKey,
@@ -69,6 +71,8 @@ describe("LocalBackedSessionStorage", () => {
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test");
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
encrypted,
sessionKey,
diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts
index bdc0bed80c0..900212ddefa 100644
--- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts
+++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts
@@ -198,6 +198,8 @@ export class LocalBackedSessionStorageService
private compareValues(value1: T, value2: T): boolean {
try {
return compareValues(value1, value2);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.logService.error(
`error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`,
diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts
index a2f0c78cd52..0499f34a4ae 100644
--- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts
+++ b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts
@@ -17,6 +17,8 @@ const supported = (() => {
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// ignore
}
diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts
index e8a660620a9..9d4835889b9 100644
--- a/apps/browser/src/popup/app.component.ts
+++ b/apps/browser/src/popup/app.component.ts
@@ -21,7 +21,7 @@ import {
ToastOptions,
ToastService,
} from "@bitwarden/components";
-import { BiometricStateService } from "@bitwarden/key-management";
+import { BiometricsService, BiometricStateService } from "@bitwarden/key-management";
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
@@ -66,6 +66,7 @@ export class AppComponent implements OnInit, OnDestroy {
private accountService: AccountService,
private animationControlService: AnimationControlService,
private biometricStateService: BiometricStateService,
+ private biometricsService: BiometricsService,
) {}
async ngOnInit() {
@@ -102,7 +103,7 @@ export class AppComponent implements OnInit, OnDestroy {
this.messageListener.allMessages$
.pipe(
- tap((msg: any) => {
+ tap(async (msg: any) => {
if (msg.command === "doneLoggingOut") {
// TODO: PM-8544 - why do we call logout in the popup after receiving the doneLoggingOut message? Hasn't this already completeted logout?
this.authService.logOut(async () => {
@@ -119,6 +120,7 @@ export class AppComponent implements OnInit, OnDestroy {
msg.command === "locked" &&
(msg.userId == null || msg.userId == this.activeUserId)
) {
+ await this.biometricsService.setShouldAutopromptNow(false);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["lock"]);
@@ -135,8 +137,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.toastService._showToast(msg);
} else if (msg.command === "reloadProcess") {
if (this.platformUtilsService.isSafari()) {
- window.setTimeout(() => {
- this.biometricStateService.updateLastProcessReload();
+ window.setTimeout(async () => {
+ await this.biometricStateService.updateLastProcessReload();
window.location.reload();
}, 2000);
}
diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts
index 8ffe0743bf6..dadd7917b99 100644
--- a/apps/browser/src/popup/main.ts
+++ b/apps/browser/src/popup/main.ts
@@ -4,7 +4,9 @@ import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./scss/popup.scss");
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./scss/tailwind.css");
import { AppModule } from "./app.module";
diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts
index 432754ab91c..21fa44b8d3f 100644
--- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts
+++ b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts
@@ -12,6 +12,8 @@ describe("LpFilelessImporter", () => {
chrome.runtime.connect = jest.fn(() => portSpy);
beforeEach(() => {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./lp-fileless-importer");
lpFilelessImporter = (globalThis as any).lpFilelessImporter;
});
diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts
index 95b49ea00ec..8479235cc17 100644
--- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts
+++ b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts
@@ -7,6 +7,8 @@ describe("LP Suppress Import Download for Manifest v2", () => {
return node;
});
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./lp-suppress-import-download-script-append.mv2");
expect(window.document.createElement).toHaveBeenCalledWith("script");
diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts
index bfff3787506..ff0ed381599 100644
--- a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts
+++ b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts
@@ -10,6 +10,8 @@ describe("LP Suppress Import Download", () => {
jest.spyOn(Element.prototype, "appendChild");
jest.spyOn(window, "addEventListener");
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require("./lp-suppress-import-download");
anchor = document.createElement("a");
diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html
index 7c4ea3e5b46..eae8e2cc980 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html
@@ -1,7 +1,7 @@
=
+ this.vaultPopupAutofillService.currentTabIsOnBlocklist$;
+
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupAutofillService: VaultPopupAutofillService,
diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html
new file mode 100644
index 00000000000..05db600bd5a
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html
@@ -0,0 +1,10 @@
+
+ {{ "autofillBlockedNoticeV2" | i18n }}
+
+ {{ "autofillBlockedNoticeGuidance" | i18n }}
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts
new file mode 100644
index 00000000000..3a17825f4fb
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts
@@ -0,0 +1,53 @@
+import { CommonModule } from "@angular/common";
+import { Component, OnInit } from "@angular/core";
+import { RouterModule } from "@angular/router";
+import { Observable } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import {
+ BannerModule,
+ IconButtonModule,
+ LinkModule,
+ TypographyModule,
+} from "@bitwarden/components";
+
+import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
+
+const blockedURISettingsRoute = "/blocked-domains";
+
+@Component({
+ standalone: true,
+ imports: [
+ BannerModule,
+ CommonModule,
+ IconButtonModule,
+ JslibModule,
+ LinkModule,
+ RouterModule,
+ TypographyModule,
+ ],
+ selector: "blocked-injection-banner",
+ templateUrl: "blocked-injection-banner.component.html",
+})
+export class BlockedInjectionBanner implements OnInit {
+ /**
+ * Flag indicating that the banner should be shown
+ */
+ protected showCurrentTabIsBlockedBanner$: Observable =
+ this.vaultPopupAutofillService.showCurrentTabIsBlockedBanner$;
+
+ /**
+ * Hostname for current tab
+ */
+ protected currentTabHostname?: string;
+
+ blockedURISettingsRoute: string = blockedURISettingsRoute;
+
+ constructor(private vaultPopupAutofillService: VaultPopupAutofillService) {}
+
+ async ngOnInit() {}
+
+ async handleCurrentTabIsBlockedBannerDismiss() {
+ await this.vaultPopupAutofillService.dismissCurrentTabIsBlockedBanner();
+ }
+}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts
index 38ec6056d19..1f67dd51c21 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts
@@ -152,7 +152,7 @@ describe("VaultHeaderV2Component", () => {
it("defaults the initial state to true", (done) => {
// The initial value of the `state$` variable above is undefined
component["initialDisclosureVisibility$"].subscribe((initialVisibility) => {
- expect(initialVisibility).toBeTrue();
+ expect(initialVisibility).toBe(true);
done();
});
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
index b5d92a386b3..c55e8d9fb26 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
@@ -1,23 +1,76 @@
0 || description" [disableMargin]="disableSectionMargin">
-
-
-
- {{ title }}
-
-
- {{ ciphers.length }}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+ {{ ciphers.length }}
+
+
+
+
+
+
+
+
+
{{ description }}
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
index a3d8f3ffe31..7c21c7e6a0c 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
@@ -19,8 +19,8 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
-import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
+import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import {
NewItemDropdownV2Component,
NewItemInitialValues,
@@ -40,6 +40,7 @@ enum VaultState {
templateUrl: "vault-v2.component.html",
standalone: true,
imports: [
+ BlockedInjectionBanner,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
@@ -56,7 +57,6 @@ enum VaultState {
VaultHeaderV2Component,
DecryptionFailureDialogComponent,
],
- providers: [VaultUiOnboardingService],
})
export class VaultV2Component implements OnInit, OnDestroy {
cipherType = CipherType;
@@ -91,7 +91,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupListFiltersService: VaultPopupListFiltersService,
- private vaultUiOnboardingService: VaultUiOnboardingService,
private destroyRef: DestroyRef,
private cipherService: CipherService,
private dialogService: DialogService,
@@ -121,8 +120,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
}
async ngOnInit() {
- await this.vaultUiOnboardingService.showOnboardingDialog();
-
this.cipherService.failedToDecryptCiphers$
.pipe(
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts
index 7ee15aa833b..526ab2e2579 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts
@@ -179,7 +179,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
- expect(doAutofill).toHaveBeenCalledOnce();
+ expect(doAutofill).toHaveBeenCalledTimes(1);
}));
it('invokes `copy` when action="copy-username"', fakeAsync(() => {
@@ -187,7 +187,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
- expect(copy).toHaveBeenCalledOnce();
+ expect(copy).toHaveBeenCalledTimes(1);
}));
it('invokes `copy` when action="copy-password"', fakeAsync(() => {
@@ -195,7 +195,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
- expect(copy).toHaveBeenCalledOnce();
+ expect(copy).toHaveBeenCalledTimes(1);
}));
it('invokes `copy` when action="copy-totp"', fakeAsync(() => {
@@ -203,7 +203,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
- expect(copy).toHaveBeenCalledOnce();
+ expect(copy).toHaveBeenCalledTimes(1);
}));
it("closes the popout after a load action", fakeAsync(() => {
@@ -218,9 +218,9 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
- expect(doAutofill).toHaveBeenCalledOnce();
+ expect(doAutofill).toHaveBeenCalledTimes(1);
expect(focusSpy).toHaveBeenCalledWith(99);
- expect(closeSpy).toHaveBeenCalledOnce();
+ expect(closeSpy).toHaveBeenCalledTimes(1);
}));
});
});
diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts
index ae8d4666651..2dad1e3034c 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts
@@ -4,6 +4,7 @@ import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -44,6 +45,7 @@ describe("VaultPopupAutofillService", () => {
// Create mocks for VaultPopupAutofillService
const mockAutofillService = mock();
+ const mockDomainSettingsService = mock();
const mockI18nService = mock();
const mockToastService = mock();
const mockPlatformUtilsService = mock();
@@ -71,6 +73,7 @@ describe("VaultPopupAutofillService", () => {
testBed = TestBed.configureTestingModule({
providers: [
{ provide: AutofillService, useValue: mockAutofillService },
+ { provide: DomainSettingsService, useValue: mockDomainSettingsService },
{ provide: I18nService, useValue: mockI18nService },
{ provide: ToastService, useValue: mockToastService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts
index 586e9182819..ff282d7a6d0 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts
@@ -15,6 +15,7 @@ import {
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -67,6 +68,74 @@ export class VaultPopupAutofillService {
shareReplay({ refCount: false, bufferSize: 1 }),
);
+ currentTabIsOnBlocklist$: Observable = combineLatest([
+ this.domainSettingsService.blockedInteractionsUris$,
+ this.currentAutofillTab$,
+ ]).pipe(
+ map(([blockedInteractionsUris, currentTab]) => {
+ if (blockedInteractionsUris && currentTab?.url?.length) {
+ const tabURL = new URL(currentTab.url);
+ const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname);
+
+ if (tabIsBlocked) {
+ return true;
+ }
+ }
+
+ return false;
+ }),
+ shareReplay({ refCount: false, bufferSize: 1 }),
+ );
+
+ showCurrentTabIsBlockedBanner$: Observable = combineLatest([
+ this.domainSettingsService.blockedInteractionsUris$,
+ this.currentAutofillTab$,
+ ]).pipe(
+ map(([blockedInteractionsUris, currentTab]) => {
+ if (blockedInteractionsUris && currentTab?.url?.length) {
+ const tabURL = new URL(currentTab.url);
+ const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname);
+
+ const showScriptInjectionIsBlockedBanner =
+ tabIsBlocked && !blockedInteractionsUris[tabURL.hostname]?.bannerIsDismissed;
+
+ return showScriptInjectionIsBlockedBanner;
+ }
+
+ return false;
+ }),
+ shareReplay({ refCount: false, bufferSize: 1 }),
+ );
+
+ async dismissCurrentTabIsBlockedBanner() {
+ try {
+ const currentTab = await firstValueFrom(this.currentAutofillTab$);
+ const currentTabURL = currentTab?.url.length && new URL(currentTab.url);
+
+ const currentTabHostname = currentTabURL && currentTabURL.hostname;
+
+ if (!currentTabHostname) {
+ return;
+ }
+
+ const blockedURIs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$);
+ const tabIsBlocked = Object.keys(blockedURIs).includes(currentTabHostname);
+
+ if (tabIsBlocked) {
+ void this.domainSettingsService.setBlockedInteractionsUris({
+ ...blockedURIs,
+ [currentTabHostname as string]: { bannerIsDismissed: true },
+ });
+ }
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (e) {
+ throw new Error(
+ "There was a problem dismissing the blocked interaction URI notification banner",
+ );
+ }
+ }
+
/**
* 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.
@@ -125,6 +194,7 @@ export class VaultPopupAutofillService {
constructor(
private autofillService: AutofillService,
+ private domainSettingsService: DomainSettingsService,
private i18nService: I18nService,
private toastService: ToastService,
private platformUtilService: PlatformUtilsService,
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
index 0eb91c6cbe2..e1236be08f9 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
@@ -488,7 +488,7 @@ describe("VaultPopupListFiltersService", () => {
state$.next(true);
service.filterVisibilityState$.subscribe((filterVisibility) => {
- expect(filterVisibility).toBeTrue();
+ expect(filterVisibility).toBe(true);
done();
});
});
@@ -496,7 +496,7 @@ describe("VaultPopupListFiltersService", () => {
it("updates stored filter state", async () => {
await service.updateFilterVisibility(false);
- expect(update).toHaveBeenCalledOnce();
+ expect(update).toHaveBeenCalledTimes(1);
// Get callback passed to `update`
const updateCallback = update.mock.calls[0][0];
expect(updateCallback()).toBe(false);
diff --git a/apps/browser/src/vault/popup/services/vault-popup-section.service.ts b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts
new file mode 100644
index 00000000000..ed641e0cdf7
--- /dev/null
+++ b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts
@@ -0,0 +1,129 @@
+import { computed, effect, inject, Injectable, signal, Signal } from "@angular/core";
+import { toSignal } from "@angular/core/rxjs-interop";
+import { map } from "rxjs";
+
+import {
+ KeyDefinition,
+ StateProvider,
+ VAULT_SETTINGS_DISK,
+} from "@bitwarden/common/platform/state";
+
+import { VaultPopupItemsService } from "./vault-popup-items.service";
+
+export type PopupSectionOpen = {
+ favorites: boolean;
+ allItems: boolean;
+};
+
+const SECTION_OPEN_KEY = new KeyDefinition(VAULT_SETTINGS_DISK, "sectionOpen", {
+ deserializer: (obj) => obj,
+});
+
+const INITIAL_OPEN: PopupSectionOpen = {
+ favorites: true,
+ allItems: true,
+};
+
+@Injectable({
+ providedIn: "root",
+})
+export class VaultPopupSectionService {
+ private vaultPopupItemsService = inject(VaultPopupItemsService);
+ private stateProvider = inject(StateProvider);
+
+ private hasFilterOrSearchApplied = toSignal(
+ this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)),
+ );
+
+ /**
+ * Used to change the open/close state without persisting it to the local disk. Reflects
+ * application-applied overrides.
+ * `null` means there is no current override
+ */
+ private temporaryStateOverride = signal | null>(null);
+
+ constructor() {
+ effect(
+ () => {
+ /**
+ * auto-open all sections when search or filter is applied, and remove
+ * override when search or filter is removed
+ */
+ if (this.hasFilterOrSearchApplied()) {
+ this.temporaryStateOverride.set(INITIAL_OPEN);
+ } else {
+ this.temporaryStateOverride.set(null);
+ }
+ },
+ {
+ allowSignalWrites: true,
+ },
+ );
+ }
+
+ /**
+ * Stored disk state for the open/close state of the sections. Will be `null` if user has never
+ * opened/closed a section
+ */
+ private sectionOpenStateProvider = this.stateProvider.getGlobal(SECTION_OPEN_KEY);
+
+ /**
+ * Stored disk state for the open/close state of the sections, with an initial value provided
+ * if the stored disk state does not yet exist.
+ */
+ private sectionOpenStoredState = toSignal(
+ this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)),
+ // Indicates that the state value is loading
+ { initialValue: null },
+ );
+
+ /**
+ * Indicates the current open/close display state of each section, accounting for temporary
+ * non-persisted overrides.
+ */
+ sectionOpenDisplayState: Signal> = computed(() => ({
+ ...this.sectionOpenStoredState(),
+ ...this.temporaryStateOverride(),
+ }));
+
+ /**
+ * Retrieve the open/close display state for a given section.
+ *
+ * @param sectionKey section key
+ */
+ getOpenDisplayStateForSection(sectionKey: keyof PopupSectionOpen): Signal {
+ return computed(() => this.sectionOpenDisplayState()?.[sectionKey]);
+ }
+
+ /**
+ * Updates the stored open/close state of a given section. Should be called only when a user action
+ * is taken directly to change the open/close state.
+ *
+ * Removes any current temporary override for the given section, as direct user action should
+ * supersede any application-applied overrides.
+ *
+ * @param sectionKey section key
+ */
+ async updateSectionOpenStoredState(
+ sectionKey: keyof PopupSectionOpen,
+ open: boolean,
+ ): Promise {
+ await this.sectionOpenStateProvider.update((currentState) => {
+ return {
+ ...(currentState ?? INITIAL_OPEN),
+ [sectionKey]: open,
+ };
+ });
+
+ this.temporaryStateOverride.update((prev) => {
+ if (prev !== null) {
+ return {
+ ...prev,
+ [sectionKey]: open,
+ };
+ }
+
+ return prev;
+ });
+ }
+}
diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts
deleted file mode 100644
index f50d6ebc236..00000000000
--- a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Injectable } from "@angular/core";
-import { firstValueFrom, map } from "rxjs";
-
-import { ApiService } from "@bitwarden/common/abstractions/api.service";
-import {
- GlobalState,
- KeyDefinition,
- StateProvider,
- VAULT_BROWSER_UI_ONBOARDING,
-} from "@bitwarden/common/platform/state";
-import { DialogService } from "@bitwarden/components";
-
-import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component";
-
-// Key definition for the Vault UI onboarding state.
-// This key is used to store the state of the new UI information dialog.
-export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition(
- VAULT_BROWSER_UI_ONBOARDING,
- "dialogState",
- {
- deserializer: (obj) => obj,
- },
-);
-
-@Injectable()
-export class VaultUiOnboardingService {
- private onboardingUiReleaseDate = new Date("2024-12-10");
-
- private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal(
- GLOBAL_VAULT_UI_ONBOARDING,
- );
-
- private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe(
- map((x) => x ?? false),
- );
-
- constructor(
- private stateProvider: StateProvider,
- private dialogService: DialogService,
- private apiService: ApiService,
- ) {}
-
- /**
- * Checks whether the onboarding dialog should be shown and opens it if necessary.
- * The dialog is shown if the user has not previously viewed it and is not a new account.
- */
- async showOnboardingDialog(): Promise {
- const hasViewedDialog = await this.getVaultUiOnboardingState();
-
- if (!hasViewedDialog && !(await this.isNewAccount())) {
- await this.openVaultUiOnboardingDialog();
- }
- }
-
- private async openVaultUiOnboardingDialog(): Promise {
- const dialogRef = VaultUiOnboardingComponent.open(this.dialogService);
-
- const result = firstValueFrom(dialogRef.closed);
-
- // Update the onboarding state when the dialog is closed
- await this.setVaultUiOnboardingState(true);
-
- return result;
- }
-
- private async isNewAccount(): Promise {
- const userProfile = await this.apiService.getProfile();
- const profileCreationDate = new Date(userProfile.creationDate);
- return profileCreationDate > this.onboardingUiReleaseDate;
- }
-
- /**
- * Updates and saves the state indicating whether the user has viewed
- * the new UI onboarding information dialog.
- */
- private async setVaultUiOnboardingState(value: boolean): Promise {
- await this.vaultUiOnboardingState.update(() => value);
- }
-
- /**
- * Retrieves the current state indicating whether the user has viewed
- * the new UI onboarding information dialog.s
- */
- private async getVaultUiOnboardingState(): Promise {
- return await firstValueFrom(this.vaultUiOnboardingState$);
- }
-}
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 28432bd5558..d1d8ac76ec4 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.",
- "version": "2025.1.0",
+ "version": "2025.1.1",
"keywords": [
"bitwarden",
"password",
@@ -63,7 +63,7 @@
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "11.1.0",
- "form-data": "4.0.0",
+ "form-data": "4.0.1",
"https-proxy-agent": "7.0.5",
"inquirer": "8.2.6",
"jsdom": "25.0.1",
diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts
index 5b351efe459..e26d073326e 100644
--- a/apps/cli/src/admin-console/commands/share.command.ts
+++ b/apps/cli/src/admin-console/commands/share.command.ts
@@ -34,6 +34,8 @@ export class ShareCommand {
if (req == null || req.length === 0) {
return Response.badRequest("You must provide at least one collection id for this item.");
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts
index 2a3d5d85408..359ed08ca99 100644
--- a/apps/cli/src/auth/commands/login.command.ts
+++ b/apps/cli/src/auth/commands/login.command.ts
@@ -165,6 +165,8 @@ export class LoginCommand {
if (options.method != null) {
twoFactorMethod = parseInt(options.method, null);
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.error("Invalid two-step login method.");
}
@@ -240,6 +242,8 @@ export class LoginCommand {
if (twoFactorMethod != null) {
try {
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.error("Invalid two-step login method.");
}
diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts
index dd99d03b086..8efb414f5b2 100644
--- a/apps/cli/src/commands/edit.command.ts
+++ b/apps/cli/src/commands/edit.command.ts
@@ -57,6 +57,8 @@ export class EditCommand {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
diff --git a/apps/cli/src/platform/flags.ts b/apps/cli/src/platform/flags.ts
index dc0103e2436..a762053da35 100644
--- a/apps/cli/src/platform/flags.ts
+++ b/apps/cli/src/platform/flags.ts
@@ -7,11 +7,9 @@ import {
} from "@bitwarden/common/platform/misc/flags";
// required to avoid linting errors when there are no flags
-// eslint-disable-next-line @typescript-eslint/ban-types
export type Flags = {} & SharedFlags;
// required to avoid linting errors when there are no flags
-// eslint-disable-next-line @typescript-eslint/ban-types
export type DevFlags = {} & SharedDevFlags;
export function flagEnabled(flag: keyof Flags): boolean {
diff --git a/apps/cli/src/platform/services/node-env-secure-storage.service.ts b/apps/cli/src/platform/services/node-env-secure-storage.service.ts
index 5e9fdd26b3c..2807509e428 100644
--- a/apps/cli/src/platform/services/node-env-secure-storage.service.ts
+++ b/apps/cli/src/platform/services/node-env-secure-storage.service.ts
@@ -87,6 +87,8 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
}
return Utils.fromBufferToB64(decValue);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.logService.info("Decrypt error.");
return null;
@@ -104,6 +106,8 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
}
}
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.logService.info("Session key is invalid.");
}
diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts
index eff351be22a..a9264c50126 100644
--- a/apps/cli/src/tools/send/commands/create.command.ts
+++ b/apps/cli/src/tools/send/commands/create.command.ts
@@ -49,6 +49,8 @@ export class SendCreateCommand {
if (req == null) {
throw new Error("Null request");
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts
index 11508d5c417..ed719b58311 100644
--- a/apps/cli/src/tools/send/commands/edit.command.ts
+++ b/apps/cli/src/tools/send/commands/edit.command.ts
@@ -41,6 +41,8 @@ export class SendEditCommand {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts
index d27ba4f88ec..41a6681af55 100644
--- a/apps/cli/src/tools/send/commands/receive.command.ts
+++ b/apps/cli/src/tools/send/commands/receive.command.ts
@@ -47,6 +47,8 @@ export class SendReceiveCommand extends DownloadCommand {
let urlObject: URL;
try {
urlObject = new URL(url);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.badRequest("Failed to parse the provided Send url");
}
diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts
index 13cd666754f..a28d070d19e 100644
--- a/apps/cli/src/vault/create.command.ts
+++ b/apps/cli/src/vault/create.command.ts
@@ -66,6 +66,8 @@ export class CreateCommand {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
diff --git a/apps/desktop/config/config.js b/apps/desktop/config/config.js
index 404295dd0db..30a5c16bb2a 100644
--- a/apps/desktop/config/config.js
+++ b/apps/desktop/config/config.js
@@ -32,6 +32,8 @@ function log(configObj) {
function loadConfig(configName) {
try {
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
return require(`./${configName}.json`);
} catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs
index 32300b9f81f..8b297fc33b7 100644
--- a/apps/desktop/desktop_native/core/src/password/windows.rs
+++ b/apps/desktop/desktop_native/core/src/password/windows.rs
@@ -13,7 +13,7 @@ use windows::{
const CRED_FLAGS_NONE: u32 = 0;
-pub async fn get_password<'a>(service: &str, account: &str) -> Result {
+pub async fn get_password(service: &str, account: &str) -> Result {
let target_name = U16CString::from_str(target_name(service, account))?;
let mut credential: *mut CREDENTIALW = std::ptr::null_mut();
diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json
index c8114d947e4..4302f302473 100644
--- a/apps/desktop/electron-builder.json
+++ b/apps/desktop/electron-builder.json
@@ -20,7 +20,7 @@
"**/node_modules/@bitwarden/desktop-napi/index.js",
"**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node"
],
- "electronVersion": "33.2.1",
+ "electronVersion": "33.3.1",
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index c55f39d1b2a..aff1d0ffbb0 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
- "version": "2025.1.0",
+ "version": "2025.1.3",
"keywords": [
"bitwarden",
"password",
diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js
index c4513687e89..c39e7ea0355 100644
--- a/apps/desktop/postcss.config.js
+++ b/apps/desktop/postcss.config.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-undef */
+/* eslint-disable @typescript-eslint/no-require-imports, no-undef */
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
};
diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml
index 02f2474927e..fea28052f8d 100644
--- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml
+++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml
@@ -8,7 +8,6 @@ command: bitwarden.sh
finish-args:
- --share=ipc
- --share=network
- - --socket=wayland
- --socket=x11
- --device=dri
- --env=XDG_CURRENT_DESKTOP=Unity
@@ -43,5 +42,4 @@ modules:
commands:
- ulimit -c 0
- export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
- - exec zypak-wrapper /app/bin/bitwarden-app --ozone-platform-hint=auto
- --enable-features=WaylandWindowDecorations "$@"
+ - exec zypak-wrapper /app/bin/bitwarden-app "$@"
diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js
index fd16cd5ffbe..30b17a44d12 100644
--- a/apps/desktop/scripts/after-pack.js
+++ b/apps/desktop/scripts/after-pack.js
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-var-requires, no-console */
+/* eslint-disable @typescript-eslint/no-require-imports, no-console */
require("dotenv").config();
const child_process = require("child_process");
const path = require("path");
diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js
index dc60e9d1838..20c24c8a76b 100644
--- a/apps/desktop/scripts/after-sign.js
+++ b/apps/desktop/scripts/after-sign.js
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-var-requires, no-console */
+/* eslint-disable @typescript-eslint/no-require-imports, no-console */
require("dotenv").config();
const path = require("path");
diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js
index 4cb643c34d4..649fe3b6736 100644
--- a/apps/desktop/scripts/build-macos-extension.js
+++ b/apps/desktop/scripts/build-macos-extension.js
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-var-requires, no-console */
+/* eslint-disable @typescript-eslint/no-require-imports, no-console */
const child = require("child_process");
const { exit } = require("process");
diff --git a/apps/desktop/scripts/start.js b/apps/desktop/scripts/start.js
index c1a2fb3cffc..d2c984a6f24 100644
--- a/apps/desktop/scripts/start.js
+++ b/apps/desktop/scripts/start.js
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
+/* eslint-disable @typescript-eslint/no-require-imports */
const concurrently = require("concurrently");
const rimraf = require("rimraf");
diff --git a/apps/desktop/sign.js b/apps/desktop/sign.js
index 74c63932306..f0110ea195b 100644
--- a/apps/desktop/sign.js
+++ b/apps/desktop/sign.js
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-var-requires, no-console */
+/* eslint-disable @typescript-eslint/no-require-imports, no-console */
exports.default = async function (configuration) {
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts
index 19748e797bb..f3440975cf2 100644
--- a/apps/desktop/src/app/accounts/settings.component.ts
+++ b/apps/desktop/src/app/accounts/settings.component.ts
@@ -650,7 +650,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
const skipSupportedPlatformCheck =
ipc.platform.allowBrowserintegrationOverride || ipc.platform.isDev;
- if (skipSupportedPlatformCheck) {
+ if (!skipSupportedPlatformCheck) {
if (
ipc.platform.deviceType === DeviceType.MacOsDesktop &&
!this.platformUtilsService.isMacAppStore()
diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts
index e4ee5ec0473..e565681de93 100644
--- a/apps/desktop/src/app/app-routing.module.ts
+++ b/apps/desktop/src/app/app-routing.module.ts
@@ -67,6 +67,7 @@ import { SendComponent } from "./tools/send/send.component";
/**
* Data properties acceptable for use in route objects in the desktop
*/
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface RouteDataProperties {
// For any new route data properties, add them here.
// then assert that the data object satisfies this interface in the route object.
diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts
index 5fefbf9ddff..a05b09e139e 100644
--- a/apps/desktop/src/app/app.component.ts
+++ b/apps/desktop/src/app/app.component.ts
@@ -330,6 +330,20 @@ export class AppComponent implements OnInit, OnDestroy {
}
break;
}
+ case "upgradeOrganization": {
+ const upgradeConfirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "upgradeOrganization" },
+ content: { key: "upgradeOrganizationDesc" },
+ acceptButtonText: { key: "learnMore" },
+ type: "info",
+ });
+ if (upgradeConfirmed) {
+ this.platformUtilsService.launchUri(
+ "https://bitwarden.com/help/upgrade-from-individual-to-org/",
+ );
+ }
+ break;
+ }
case "emailVerificationRequired": {
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "emailVerificationRequired" },
diff --git a/apps/desktop/src/app/main.ts b/apps/desktop/src/app/main.ts
index a0b490edaa6..287d66795d2 100644
--- a/apps/desktop/src/app/main.ts
+++ b/apps/desktop/src/app/main.ts
@@ -1,7 +1,9 @@
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("../scss/styles.scss");
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require("../scss/tailwind.css");
import { AppModule } from "./app.module";
diff --git a/apps/desktop/src/auth/login/desktop-login-component.service.ts b/apps/desktop/src/auth/login/desktop-login-component.service.ts
index 8a2daef49ff..dbf689e801c 100644
--- a/apps/desktop/src/auth/login/desktop-login-component.service.ts
+++ b/apps/desktop/src/auth/login/desktop-login-component.service.ts
@@ -65,6 +65,8 @@ export class DesktopLoginComponentService
try {
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
this.toastService.showToast({
variant: "error",
diff --git a/apps/desktop/src/auth/login/login-v1.component.ts b/apps/desktop/src/auth/login/login-v1.component.ts
index 5b1a1c68d29..e0c3f794dba 100644
--- a/apps/desktop/src/auth/login/login-v1.component.ts
+++ b/apps/desktop/src/auth/login/login-v1.component.ts
@@ -249,6 +249,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
try {
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state);
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
this.platformUtilsService.showToast(
"error",
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 1a02e5db4e7..bca12f16a7d 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -3474,5 +3474,14 @@
},
"changeAcctEmail": {
"message": "Change account email"
+ },
+ "organizationUpgradeRequired": {
+ "message": "Organization upgrade required"
+ },
+ "upgradeOrganization": {
+ "message": "Upgrade organization"
+ },
+ "upgradeOrganizationDesc": {
+ "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
}
}
diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts
index 6915a18deda..107d546811c 100644
--- a/apps/desktop/src/main/native-messaging.main.ts
+++ b/apps/desktop/src/main/native-messaging.main.ts
@@ -405,6 +405,8 @@ export class NativeMessagingMain {
this.logService.info(`Error reading preferences: ${e}`);
}
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// Browser is not installed, we can just skip it
}
diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json
index f93d9059bc7..e825bd41581 100644
--- a/apps/desktop/src/package-lock.json
+++ b/apps/desktop/src/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
- "version": "2025.1.0",
+ "version": "2025.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
- "version": "2025.1.0",
+ "version": "2025.1.3",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json
index 39e16815020..6feed970798 100644
--- a/apps/desktop/src/package.json
+++ b/apps/desktop/src/package.json
@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
- "version": "2025.1.0",
+ "version": "2025.1.3",
"author": "Bitwarden Inc. (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
diff --git a/apps/desktop/src/platform/flags.ts b/apps/desktop/src/platform/flags.ts
index dc0103e2436..a762053da35 100644
--- a/apps/desktop/src/platform/flags.ts
+++ b/apps/desktop/src/platform/flags.ts
@@ -7,11 +7,9 @@ import {
} from "@bitwarden/common/platform/misc/flags";
// required to avoid linting errors when there are no flags
-// eslint-disable-next-line @typescript-eslint/ban-types
export type Flags = {} & SharedFlags;
// required to avoid linting errors when there are no flags
-// eslint-disable-next-line @typescript-eslint/ban-types
export type DevFlags = {} & SharedDevFlags;
export function flagEnabled(flag: keyof Flags): boolean {
diff --git a/apps/desktop/src/platform/services/ssh-agent.service.ts b/apps/desktop/src/platform/services/ssh-agent.service.ts
index 726d28022e5..d4c7c5f460e 100644
--- a/apps/desktop/src/platform/services/ssh-agent.service.ts
+++ b/apps/desktop/src/platform/services/ssh-agent.service.ts
@@ -83,6 +83,15 @@ export class SshAgentService implements OnDestroy {
this.messageListener
.messages$(new CommandDefinition("sshagent.signrequest"))
.pipe(
+ withLatestFrom(this.desktopSettingsService.sshAgentEnabled$),
+ concatMap(async ([message, enabled]) => {
+ if (!enabled) {
+ await ipc.platform.sshAgent.signRequestResponse(message.requestId as number, false);
+ }
+ return { message, enabled };
+ }),
+ filter(({ enabled }) => enabled),
+ map(({ message }) => message),
withLatestFrom(this.authService.activeAccountStatus$),
// This switchMap handles unlocking the vault if it is locked:
// - If the vault is locked, we will wait for it to be unlocked.
diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts
index ea1e7e76c56..74f80785fca 100644
--- a/apps/desktop/src/services/biometric-message-handler.service.ts
+++ b/apps/desktop/src/services/biometric-message-handler.service.ts
@@ -188,7 +188,7 @@ export class BiometricMessageHandlerService {
appId,
);
}
- // TODO: legacy, remove after 2025.01
+ // TODO: legacy, remove after 2025.3
case BiometricsCommands.IsAvailable: {
const available =
(await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available;
@@ -200,7 +200,7 @@ export class BiometricMessageHandlerService {
appId,
);
}
- // TODO: legacy, remove after 2025.01
+ // TODO: legacy, remove after 2025.3
case BiometricsCommands.Unlock: {
const isTemporarilyDisabled =
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
@@ -265,6 +265,8 @@ export class BiometricMessageHandlerService {
} else {
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}
@@ -342,6 +344,8 @@ export class BiometricMessageHandlerService {
appId,
);
}
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
await this.send(
{ command: BiometricsCommands.UnlockWithBiometricsForUser, messageId, response: false },
diff --git a/apps/desktop/src/services/duckduckgo-message-handler.service.ts b/apps/desktop/src/services/duckduckgo-message-handler.service.ts
index 9a914f238b5..fa5c2f4d9f7 100644
--- a/apps/desktop/src/services/duckduckgo-message-handler.service.ts
+++ b/apps/desktop/src/services/duckduckgo-message-handler.service.ts
@@ -130,6 +130,8 @@ export class DuckDuckGoMessageHandlerService {
sharedKey: Utils.fromBufferToB64(encryptedSecret),
},
});
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
this.sendResponse({
messageId: messageId,
@@ -153,6 +155,8 @@ export class DuckDuckGoMessageHandlerService {
await this.encryptedMessageHandlerService.responseDataForCommand(decryptedCommandData);
await this.sendEncryptedResponse(message, { command, payload: responseData });
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts
index 09c0fe2a07c..43c4b9065a7 100644
--- a/apps/desktop/src/services/encrypted-message-handler.service.ts
+++ b/apps/desktop/src/services/encrypted-message-handler.service.ts
@@ -178,6 +178,8 @@ export class EncryptedMessageHandlerService {
await this.messagingService.send("refreshCiphers");
return { status: "success" };
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return { status: "failure" };
}
@@ -222,6 +224,8 @@ export class EncryptedMessageHandlerService {
await this.messagingService.send("refreshCiphers");
return { status: "success" };
+ // FIXME: Remove when updating file. Eslint update
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return { status: "failure" };
}
diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html
index 59e609312d7..f589ba53046 100644
--- a/apps/desktop/src/vault/app/vault/view.component.html
+++ b/apps/desktop/src/vault/app/vault/view.component.html
@@ -186,6 +186,16 @@