diff --git a/.eslintrc.json b/.eslintrc.json index 3a4306c330a..c606b8f933b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -293,6 +293,12 @@ "no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }] } }, + { + "files": ["libs/tools/card/src/**/*.ts"], + "rules": { + "no-restricted-imports": ["error", { "patterns": ["@bitwarden/tools-card/*", "src/**/*"] }] + } + }, { "files": ["libs/vault/src/**/*.ts"], "rules": { diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index f58ca84a198..a320149281b 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -14,6 +14,7 @@ ./libs/tools/README.md ./libs/tools/export/vault-export/README.md ./libs/tools/send/README.md +./libs/tools/card/README.md ./libs/vault/README.md ./README.md ./LICENSE_BITWARDEN.txt diff --git a/.storybook/main.ts b/.storybook/main.ts index 28bc2aa085c..454da4377dc 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -19,6 +19,8 @@ const config: StorybookConfig = { "../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)", "../bitwarden_license/bit-web/src/**/*.mdx", "../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)", + "../libs/tools/card/src/**/*.mdx", + "../libs/tools/card/src/**/*.stories.@(js|jsx|ts|tsx)", ], addons: [ getAbsolutePath("@storybook/addon-links"), diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e52f78583d4..25386222d4e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1408,6 +1408,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -2647,6 +2653,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 350b4a8a84d..9d7644878d0 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -9,7 +9,7 @@ import { AnonLayoutWrapperDataService, } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Icon, IconModule } from "@bitwarden/components"; +import { Icon, IconModule, Translation } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -90,11 +90,11 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { } if (firstChildRouteData["pageTitle"] !== undefined) { - this.pageTitle = this.i18nService.t(firstChildRouteData["pageTitle"]); + this.pageTitle = this.handleStringOrTranslation(firstChildRouteData["pageTitle"]); } if (firstChildRouteData["pageSubtitle"] !== undefined) { - this.pageSubtitle = this.i18nService.t(firstChildRouteData["pageSubtitle"]); + this.pageSubtitle = this.handleStringOrTranslation(firstChildRouteData["pageSubtitle"]); } if (firstChildRouteData["pageIcon"] !== undefined) { @@ -132,19 +132,11 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { } if (data.pageTitle) { - this.pageTitle = this.i18nService.t(data.pageTitle); + this.pageTitle = this.handleStringOrTranslation(data.pageTitle); } if (data.pageSubtitle) { - // If you pass just a string, we translate it by default - if (typeof data.pageSubtitle === "string") { - this.pageSubtitle = this.i18nService.t(data.pageSubtitle); - } else { - // if you pass an object, you can specify if you want to translate it or not - this.pageSubtitle = data.pageSubtitle.translate - ? this.i18nService.t(data.pageSubtitle.subtitle) - : data.pageSubtitle.subtitle; - } + this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle); } if (data.pageIcon) { @@ -168,6 +160,16 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { } } + private handleStringOrTranslation(value: string | Translation): string { + if (typeof value === "string") { + // If it's a string, return it as is + return value; + } + + // If it's a Translation object, translate it + return this.i18nService.t(value.key, ...(value.placeholders ?? [])); + } + private resetPageData() { this.pageTitle = null; this.pageSubtitle = null; diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index beb07f3523a..cc4aa2f7319 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -221,8 +221,12 @@ export const DefaultContentExample: Story = { // Dynamic Content Example const initialData: ExtensionAnonLayoutWrapperData = { - pageTitle: "setAStrongPassword", - pageSubtitle: "finishCreatingYourAccountBySettingAPassword", + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, pageIcon: LockIcon, showAcctSwitcher: true, showBackButton: true, @@ -230,8 +234,12 @@ const initialData: ExtensionAnonLayoutWrapperData = { }; const changedData: ExtensionAnonLayoutWrapperData = { - pageTitle: "enterpriseSingleSignOn", - pageSubtitle: "checkYourEmail", + pageTitle: { + key: "enterpriseSingleSignOn", + }, + pageSubtitle: { + key: "checkYourEmail", + }, pageIcon: RegistrationCheckEmailIcon, showAcctSwitcher: false, showBackButton: false, diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index e91a58a84cf..abe7d097016 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -188,6 +188,8 @@ export type OverlayBackgroundExtensionMessageHandlers = { updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; checkIsFieldCurrentlyFilling: () => boolean; getAutofillInlineMenuVisibility: () => void; + getInlineMenuCardsVisibility: () => void; + getInlineMenuIdentitiesVisibility: () => void; openAutofillInlineMenu: () => void; closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void; diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index b45a4a25485..49788d67404 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -132,6 +132,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), + getInlineMenuCardsVisibility: () => this.getInlineMenuCardsVisibility(), + getInlineMenuIdentitiesVisibility: () => this.getInlineMenuIdentitiesVisibility(), openAutofillInlineMenu: () => this.openInlineMenu(false), closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender), @@ -365,7 +367,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { } } - if (!this.cardAndIdentityCiphers.size) { + if (!this.cardAndIdentityCiphers?.size) { this.cardAndIdentityCiphers = null; } @@ -1483,6 +1485,20 @@ export class OverlayBackground implements OverlayBackgroundInterface { return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); } + /** + * Gets the inline menu's visibility setting for Cards from the settings service. + */ + private async getInlineMenuCardsVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$); + } + + /** + * Gets the inline menu's visibility setting for Identities from the settings service. + */ + private async getInlineMenuIdentitiesVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$); + } + /** * Gets the user's authentication status from the auth service. */ diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index ba815a0f29a..529607949db 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,4 +1,5 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; @@ -23,7 +24,7 @@ export type AutofillExtensionMessage = { data?: { direction?: "previous" | "next" | "current"; forceCloseInlineMenu?: boolean; - inlineMenuVisibility?: number; + newSettingValue?: InlineMenuVisibilitySetting; }; }; diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts index 8d5e08fc08e..c9d86cffc5c 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts @@ -14,7 +14,7 @@ describe("AutofillInlineMenuContentService", () => { let autofillInlineMenuContentService: AutofillInlineMenuContentService; let autofillInit: AutofillInit; let sendExtensionMessageSpy: jest.SpyInstance; - let observeBodyMutationsSpy: jest.SpyInstance; + let observeContainerMutationsSpy: jest.SpyInstance; const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdleCallback(resolve)); @@ -25,8 +25,8 @@ describe("AutofillInlineMenuContentService", () => { autofillInlineMenuContentService = new AutofillInlineMenuContentService(); autofillInit = new AutofillInit(domQueryService, null, autofillInlineMenuContentService); autofillInit.init(); - observeBodyMutationsSpy = jest.spyOn( - autofillInlineMenuContentService["bodyElementMutationObserver"] as any, + observeContainerMutationsSpy = jest.spyOn( + autofillInlineMenuContentService["containerElementMutationObserver"] as any, "observe", ); sendExtensionMessageSpy = jest.spyOn( @@ -51,7 +51,7 @@ describe("AutofillInlineMenuContentService", () => { describe("extension message handlers", () => { describe("closeAutofillInlineMenu message handler", () => { beforeEach(() => { - observeBodyMutationsSpy.mockImplementation(); + observeContainerMutationsSpy.mockImplementation(); }); it("closes the inline menu button", async () => { @@ -87,9 +87,9 @@ describe("AutofillInlineMenuContentService", () => { }); it("closes both inline menu elements and removes the body element mutation observer", async () => { - const unobserveBodyElementSpy = jest.spyOn( + const unobserveContainerElementSpy = jest.spyOn( autofillInlineMenuContentService as any, - "unobserveBodyElement", + "unobserveContainerElement", ); sendMockExtensionMessage({ command: "appendAutofillInlineMenuToDom", @@ -104,7 +104,7 @@ describe("AutofillInlineMenuContentService", () => { command: "closeAutofillInlineMenu", }); - expect(unobserveBodyElementSpy).toHaveBeenCalled(); + expect(unobserveContainerElementSpy).toHaveBeenCalled(); expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", { overlayElement: AutofillOverlayElement.Button, }); @@ -127,7 +127,7 @@ describe("AutofillInlineMenuContentService", () => { .spyOn(autofillInlineMenuContentService as any, "isInlineMenuListVisible") .mockResolvedValue(true); jest.spyOn(globalThis.document.body, "appendChild"); - observeBodyMutationsSpy.mockImplementation(); + observeContainerMutationsSpy.mockImplementation(); }); describe("creating the inline menu button", () => { @@ -279,7 +279,8 @@ describe("AutofillInlineMenuContentService", () => { }); }); - describe("handleBodyElementMutationObserverUpdate", () => { + describe("handleContainerElementMutationObserverUpdate", () => { + let mockMutationRecord: MockProxy; let buttonElement: HTMLElement; let listElement: HTMLElement; let isInlineMenuListVisibleSpy: jest.SpyInstance; @@ -289,6 +290,7 @@ describe("AutofillInlineMenuContentService", () => {
`; + mockMutationRecord = mock({ target: globalThis.document.body } as any); buttonElement = document.querySelector(".overlay-button") as HTMLElement; listElement = document.querySelector(".overlay-list") as HTMLElement; autofillInlineMenuContentService["buttonElement"] = buttonElement; @@ -309,7 +311,9 @@ describe("AutofillInlineMenuContentService", () => { autofillInlineMenuContentService["buttonElement"] = undefined; autofillInlineMenuContentService["listElement"] = undefined; - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -323,7 +327,9 @@ describe("AutofillInlineMenuContentService", () => { ) .mockReturnValue(true); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -332,14 +338,18 @@ describe("AutofillInlineMenuContentService", () => { it("skips re-arranging the DOM elements if the last child of the body is non-existent", async () => { document.body.innerHTML = ""; - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", async () => { - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -349,7 +359,9 @@ describe("AutofillInlineMenuContentService", () => { listElement.remove(); isInlineMenuListVisibleSpy.mockResolvedValue(false); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -359,7 +371,9 @@ describe("AutofillInlineMenuContentService", () => { const injectedElement = document.createElement("div"); document.body.insertBefore(injectedElement, listElement); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( @@ -371,7 +385,9 @@ describe("AutofillInlineMenuContentService", () => { it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", async () => { document.body.appendChild(buttonElement); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( @@ -384,7 +400,9 @@ describe("AutofillInlineMenuContentService", () => { const injectedElement = document.createElement("div"); document.body.appendChild(injectedElement); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( @@ -409,7 +427,9 @@ describe("AutofillInlineMenuContentService", () => { 1000, ); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(persistentLastChild.style.getPropertyValue("z-index")).toBe("2147483646"); diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index 02d3ae052cc..110c1be7db8 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -30,7 +30,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private buttonElement: HTMLElement; private listElement: HTMLElement; private inlineMenuElementsMutationObserver: MutationObserver; - private bodyElementMutationObserver: MutationObserver; + private containerElementMutationObserver: MutationObserver; private mutationObserverIterations = 0; private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout; @@ -102,7 +102,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte return; } - this.unobserveBodyElement(); + this.unobserveContainerElement(); this.closeInlineMenuButton(); this.closeInlineMenuList(); }; @@ -153,7 +153,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } if (!(await this.isInlineMenuButtonVisible())) { - this.appendInlineMenuElementToBody(this.buttonElement); + this.appendInlineMenuElementToDom(this.buttonElement); this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.Button, true); } } @@ -168,7 +168,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } if (!(await this.isInlineMenuListVisible())) { - this.appendInlineMenuElementToBody(this.listElement); + this.appendInlineMenuElementToDom(this.listElement); this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.List, true); } } @@ -196,8 +196,15 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * * @param element - The inline menu element to append to the body element. */ - private appendInlineMenuElementToBody(element: HTMLElement) { - this.observeBodyElement(); + private appendInlineMenuElementToDom(element: HTMLElement) { + const parentDialogElement = globalThis.document.activeElement?.closest("dialog"); + if (parentDialogElement && parentDialogElement.open && parentDialogElement.matches(":modal")) { + this.observeContainerElement(parentDialogElement); + parentDialogElement.appendChild(element); + return; + } + + this.observeContainerElement(globalThis.document.body); globalThis.document.body.appendChild(element); } @@ -276,8 +283,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.handleInlineMenuElementMutationObserverUpdate, ); - this.bodyElementMutationObserver = new MutationObserver( - this.handleBodyElementMutationObserverUpdate, + this.containerElementMutationObserver = new MutationObserver( + this.handleContainerElementMutationObserverUpdate, ); }; @@ -306,19 +313,17 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Sets up a mutation observer for the body element. The mutation observer is used - * to ensure that the inline menu elements are always present at the bottom of the - * body element. + * Sets up a mutation observer for the element which contains the inline menu. */ - private observeBodyElement() { - this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true }); + private observeContainerElement(element: HTMLElement) { + this.containerElementMutationObserver?.observe(element, { childList: true }); } /** - * Disconnects the mutation observer for the body element. + * Disconnects the mutation observer for the element which contains the inline menu. */ - private unobserveBodyElement() { - this.bodyElementMutationObserver?.disconnect(); + private unobserveContainerElement() { + this.containerElementMutationObserver?.disconnect(); } /** @@ -370,11 +375,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Handles the mutation observer update for the body element. This method will - * ensure that the inline menu elements are always present at the bottom of the - * body element. + * Handles the mutation observer update for the element that contains the inline menu. + * This method will ensure that the inline menu elements are always present at the + * bottom of the container. */ - private handleBodyElementMutationObserverUpdate = () => { + private handleContainerElementMutationObserverUpdate = (mutations: MutationRecord[]) => { if ( (!this.buttonElement && !this.listElement) || this.isTriggeringExcessiveMutationObserverIterations() @@ -382,15 +387,18 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte return; } - requestIdleCallbackPolyfill(this.processBodyElementMutation, { timeout: 500 }); + const containerElement = mutations[0].target as HTMLElement; + requestIdleCallbackPolyfill(() => this.processContainerElementMutation(containerElement), { + timeout: 500, + }); }; /** - * Processes the mutation of the body element. Will trigger when an + * Processes the mutation of the element that contains the inline menu. Will trigger when an * idle moment in the execution of the main thread is detected. */ - private processBodyElementMutation = async () => { - const lastChild = globalThis.document.body.lastElementChild; + private processContainerElementMutation = async (containerElement: HTMLElement) => { + const lastChild = containerElement.lastElementChild; const secondToLastChild = lastChild?.previousElementSibling; const lastChildIsInlineMenuList = lastChild === this.listElement; const lastChildIsInlineMenuButton = lastChild === this.buttonElement; @@ -424,11 +432,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte (lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) || (lastChildIsInlineMenuButton && isInlineMenuListVisible) ) { - globalThis.document.body.insertBefore(this.buttonElement, this.listElement); + containerElement.insertBefore(this.buttonElement, this.listElement); return; } - globalThis.document.body.insertBefore(lastChild, this.buttonElement); + containerElement.insertBefore(lastChild, this.buttonElement); }; /** diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html index 9c7047c4cb7..ec8aeac37e9 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html @@ -46,6 +46,32 @@ +
+
+
+ + +
+
+ + +
+
+
diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts index 8adee86bcf4..7879e4b343d 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts @@ -8,10 +8,12 @@ import { InlineMenuVisibilitySetting, ClearClipboardDelaySetting, } from "@bitwarden/common/autofill/types"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy, UriMatchStrategySetting, } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -20,7 +22,6 @@ import { DialogService } from "@bitwarden/components"; import { BrowserApi } from "../../../platform/browser/browser-api"; import { enableAccountSwitching } from "../../../platform/flags"; -import { AutofillService } from "../../services/abstractions/autofill.service"; @Component({ selector: "app-autofill-v1", @@ -32,6 +33,10 @@ export class AutofillV1Component implements OnInit { protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; protected autoFillOverlayVisibilityOptions: any[]; protected disablePasswordManagerLink: string; + protected inlineMenuPositioningImprovementsEnabled: boolean = false; + protected showInlineMenuIdentities: boolean = true; + protected showInlineMenuCards: boolean = true; + inlineMenuIsEnabled: boolean = false; enableAutoFillOnPageLoad = false; autoFillOnPageLoadDefault = false; autoFillOnPageLoadOptions: any[]; @@ -50,7 +55,7 @@ export class AutofillV1Component implements OnInit { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private domainSettingsService: DomainSettingsService, - private autofillService: AutofillService, + private configService: ConfigService, private dialogService: DialogService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private messagingService: MessagingService, @@ -109,6 +114,20 @@ export class AutofillV1Component implements OnInit { this.autofillSettingsService.inlineMenuVisibility$, ); + this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); + + this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); + + this.showInlineMenuIdentities = + this.inlineMenuPositioningImprovementsEnabled && + (await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$)); + + this.showInlineMenuCards = + this.inlineMenuPositioningImprovementsEnabled && + (await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$)); + this.enableAutoFillOnPageLoad = await firstValueFrom( this.autofillSettingsService.autofillOnPageLoad$, ); @@ -140,9 +159,18 @@ export class AutofillV1Component implements OnInit { ); } + isInlineMenuEnabled() { + return ( + this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus || + this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick + ); + } + async updateAutoFillOverlayVisibility() { await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); await this.requestPrivacyPermission(); + + this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); } async updateAutoFillOnPageLoad() { @@ -298,4 +326,12 @@ export class AutofillV1Component implements OnInit { async updateShowIdentitiesCurrentTab() { await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); } + + async updateShowInlineMenuCards() { + await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards); + } + + async updateShowInlineMenuIdentities() { + await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities); + } } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 99ac51bcdbc..b9e4ad222d2 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -27,7 +27,37 @@ {{ "showInlineMenuOnFormFieldsDescAlt" | i18n }} - + + + + {{ "showInlineMenuIdentitiesLabel" | i18n }} + + + + + + {{ "showInlineMenuCardsLabel" | i18n }} + + + { it("updates the inlineMenuVisibility property", () => { sendMockExtensionMessage({ command: "updateAutofillInlineMenuVisibility", - data: { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, + data: { newSettingValue: AutofillOverlayVisibility.OnButtonClick }, }); expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual( 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 2e85fa22819..1f0a38ad806 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -8,7 +8,9 @@ import { AutofillOverlayVisibility, AUTOFILL_OVERLAY_HANDLE_REPOSITION, AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT, + AUTOFILL_OVERLAY_HANDLE_SCROLL, } from "@bitwarden/common/autofill/constants"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; import { @@ -51,7 +53,9 @@ import { AutoFillConstants } from "./autofill-constants"; export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { pageDetailsUpdateRequired = false; - inlineMenuVisibility: number; + inlineMenuVisibility: InlineMenuVisibilitySetting; + private showInlineMenuIdentities: boolean; + private showInlineMenuCards: boolean; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Map, AutofillField> = new Map(); @@ -183,6 +187,18 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, ) { + if (!this.inlineMenuVisibility) { + await this.getInlineMenuVisibility(); + } + + if (this.showInlineMenuCards == null) { + await this.getInlineMenuCardsVisibility(); + } + + if (this.showInlineMenuIdentities == null) { + await this.getInlineMenuIdentitiesVisibility(); + } + if ( this.formFieldElements.has(formFieldElement) || this.isIgnoredField(autofillFieldData, pageDetails) @@ -1019,10 +1035,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ const { width, height, top, left } = await this.getMostRecentlyFocusedFieldRects(formFieldElement); const autofillFieldData = this.formFieldElements.get(formFieldElement); + let accountCreationFieldType = null; + if ( + // user setting allows display of identities in inline menu + this.showInlineMenuIdentities && + // `showInlineMenuAccountCreation` has been set or field is filled by Login cipher (autofillFieldData?.showInlineMenuAccountCreation || autofillFieldData?.filledByCipherType === CipherType.Login) && + // field is a username field, which is relevant to both Identity and Login ciphers this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData) ) { accountCreationFieldType = this.inlineMenuFieldQualificationService.isEmailField( @@ -1125,6 +1147,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( + this.showInlineMenuCards && this.inlineMenuFieldQualificationService.isFieldForCreditCardForm( autofillFieldData, pageDetails, @@ -1135,6 +1158,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( + this.showInlineMenuIdentities && this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm( autofillFieldData, pageDetails, @@ -1146,6 +1170,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( + this.showInlineMenuIdentities && this.inlineMenuFieldQualificationService.isFieldForIdentityForm( autofillFieldData, pageDetails, @@ -1244,6 +1269,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled"); autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled"); autofillFieldData.viewable = true; + void this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData); } @@ -1266,10 +1292,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ await this.updateMostRecentlyFocusedField(formFieldElement); } - if (!this.inlineMenuVisibility) { - await this.getInlineMenuVisibility(); - } - this.setupFormFieldElementEventListeners(formFieldElement); this.setupFormSubmissionEventListeners(formFieldElement, autofillFieldData); @@ -1291,6 +1313,30 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } + /** + * Queries the background script for the autofill inline menu's Cards visibility setting. + * If the setting is not found, a default value of true will be used + * @private + */ + private async getInlineMenuCardsVisibility() { + const inlineMenuCardsVisibility = await this.sendExtensionMessage( + "getInlineMenuCardsVisibility", + ); + this.showInlineMenuCards = inlineMenuCardsVisibility ?? true; + } + + /** + * Queries the background script for the autofill inline menu's Identities visibility setting. + * If the setting is not found, a default value of true will be used + * @private + */ + private async getInlineMenuIdentitiesVisibility() { + const inlineMenuIdentitiesVisibility = await this.sendExtensionMessage( + "getInlineMenuIdentitiesVisibility", + ); + this.showInlineMenuIdentities = inlineMenuIdentitiesVisibility ?? true; + } + /** * Returns a value that indicates if we should hide the inline menu list due to a filled field. * @@ -1318,8 +1364,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * @param data - The data object from the extension message. */ private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) { - if (!isNaN(data?.inlineMenuVisibility)) { - this.inlineMenuVisibility = data.inlineMenuVisibility; + const newSettingValue = data?.newSettingValue; + + if (!isNaN(newSettingValue)) { + this.inlineMenuVisibility = newSettingValue; } } @@ -1600,15 +1648,28 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * the overlay elements on scroll or resize. */ private setOverlayRepositionEventListeners() { - const handler = this.useEventHandlersMemo( + const repositionHandler = this.useEventHandlersMemo( throttle(this.handleOverlayRepositionEvent, 250), AUTOFILL_OVERLAY_HANDLE_REPOSITION, ); - globalThis.addEventListener(EVENTS.SCROLL, handler, { + + const eventTargetDoesNotContainFocusedField = (element: Element) => + typeof element?.contains === "function" && !element.contains(this.mostRecentlyFocusedField); + const scrollHandler = this.useEventHandlersMemo( + throttle((event) => { + if (eventTargetDoesNotContainFocusedField(event.target as Element)) { + return; + } + repositionHandler(event); + }, 50), + AUTOFILL_OVERLAY_HANDLE_SCROLL, + ); + + globalThis.addEventListener(EVENTS.SCROLL, scrollHandler, { capture: true, passive: true, }); - globalThis.addEventListener(EVENTS.RESIZE, handler); + globalThis.addEventListener(EVENTS.RESIZE, repositionHandler); } /** @@ -1616,12 +1677,19 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * the overlay elements on scroll or resize. */ private removeOverlayRepositionEventListeners() { - const handler = this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; - globalThis.removeEventListener(EVENTS.SCROLL, handler, { - capture: true, - }); - globalThis.removeEventListener(EVENTS.RESIZE, handler); + globalThis.removeEventListener( + EVENTS.SCROLL, + this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_SCROLL], + { + capture: true, + }, + ); + globalThis.removeEventListener( + EVENTS.RESIZE, + this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION], + ); + delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_SCROLL]; delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 7bd08caaf33..3f33caccc41 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -75,6 +75,8 @@ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock(); let inlineMenuVisibilityMock$!: BehaviorSubject; + let showInlineMenuCardsMock$!: BehaviorSubject; + let showInlineMenuIdentitiesMock$!: BehaviorSubject; let autofillSettingsService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -98,8 +100,12 @@ describe("AutofillService", () => { beforeEach(() => { scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); + showInlineMenuCardsMock$ = new BehaviorSubject(false); + showInlineMenuIdentitiesMock$ = new BehaviorSubject(false); autofillSettingsService = mock(); autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; + autofillSettingsService.showInlineMenuCards$ = showInlineMenuCardsMock$; + autofillSettingsService.showInlineMenuIdentities$ = showInlineMenuIdentitiesMock$; activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; @@ -291,12 +297,12 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, + { newSettingValue: AutofillOverlayVisibility.OnButtonClick }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, + { newSettingValue: AutofillOverlayVisibility.OnButtonClick }, ); }); @@ -308,12 +314,12 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, + { newSettingValue: AutofillOverlayVisibility.OnFieldFocus }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, + { newSettingValue: AutofillOverlayVisibility.OnFieldFocus }, ); }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index ea68b80e84f..0b25426728e 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -130,10 +130,23 @@ export default class AutofillService implements AutofillServiceInterface { async loadAutofillScriptsOnInstall() { BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection); void this.injectAutofillScriptsInAllTabs(); + this.autofillSettingsService.inlineMenuVisibility$ .pipe(startWith(undefined), pairwise()) .subscribe(([previousSetting, currentSetting]) => - this.handleInlineMenuVisibilityChange(previousSetting, currentSetting), + this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting), + ); + + this.autofillSettingsService.showInlineMenuCards$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previousSetting, currentSetting]) => + this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting), + ); + + this.autofillSettingsService.showInlineMenuIdentities$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previousSetting, currentSetting]) => + this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting), ); } @@ -3043,27 +3056,36 @@ export default class AutofillService implements AutofillServiceInterface { } /** - * Updates the autofill inline menu visibility setting in all active tabs - * when the InlineMenuVisibilitySetting observable is updated. + * Updates the autofill inline menu visibility settings in all active tabs + * when the inlineMenuVisibility, showInlineMenuCards, or showInlineMenuIdentities + * observables are updated. * - * @param previousSetting - The previous setting value - * @param currentSetting - The current setting value + * @param oldSettingValue - The previous setting value + * @param newSettingValue - The current setting value + * @param cipherType - The cipher type of the changed inline menu setting */ - private async handleInlineMenuVisibilityChange( - previousSetting: InlineMenuVisibilitySetting, - currentSetting: InlineMenuVisibilitySetting, + private async handleInlineMenuVisibilitySettingsChange( + oldSettingValue: InlineMenuVisibilitySetting | boolean, + newSettingValue: InlineMenuVisibilitySetting | boolean, ) { - if (previousSetting === undefined || previousSetting === currentSetting) { + if (oldSettingValue === undefined || oldSettingValue === newSettingValue) { return; } - const inlineMenuPreviouslyDisabled = previousSetting === AutofillOverlayVisibility.Off; - const inlineMenuCurrentlyDisabled = currentSetting === AutofillOverlayVisibility.Off; - if (!inlineMenuPreviouslyDisabled && !inlineMenuCurrentlyDisabled) { + const isInlineMenuVisibilitySubSetting = + typeof oldSettingValue === "boolean" || typeof newSettingValue === "boolean"; + const inlineMenuPreviouslyDisabled = oldSettingValue === AutofillOverlayVisibility.Off; + const inlineMenuCurrentlyDisabled = newSettingValue === AutofillOverlayVisibility.Off; + + if ( + !isInlineMenuVisibilitySubSetting && + !inlineMenuPreviouslyDisabled && + !inlineMenuCurrentlyDisabled + ) { const tabs = await BrowserApi.tabsQuery({}); tabs.forEach((tab) => BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", { - inlineMenuVisibility: currentSetting, + newSettingValue, }), ); return; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 318b856b324..819bb2a59f9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1340,7 +1340,7 @@ export default class MainBackground { } if (!supported) { - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index e07201c78d9..1e4e28ea6d0 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -449,7 +449,9 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], data: { pageIcon: LockIcon, - pageTitle: "yourVaultIsLockedV2", + pageTitle: { + key: "yourVaultIsLockedV2", + }, showReadonlyHostname: true, showAcctSwitcher: true, } satisfies ExtensionAnonLayoutWrapperData, @@ -471,7 +473,9 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { state: "signup", - pageTitle: "createAccount", + pageTitle: { + key: "createAccount", + }, } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ { @@ -492,8 +496,12 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: "setAStrongPassword", - pageSubtitle: "finishCreatingYourAccountBySettingAPassword", + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, state: "finish-signup", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -508,8 +516,12 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { - pageTitle: "joinOrganization", - pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword", + pageTitle: { + key: "joinOrganization", + }, + pageSubtitle: { + key: "finishJoiningThisOrganizationBySettingAMasterPassword", + }, state: "set-password-jit", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 113cd736c6a..3c8752f3a76 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -81,7 +81,7 @@ export class AppComponent implements OnInit, OnDestroy { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } diff --git a/apps/browser/src/tools/popup/generator/credential-generator.component.ts b/apps/browser/src/tools/popup/generator/credential-generator.component.ts index 3285a1c4906..9c1af07efdd 100644 --- a/apps/browser/src/tools/popup/generator/credential-generator.component.ts +++ b/apps/browser/src/tools/popup/generator/credential-generator.component.ts @@ -1,4 +1,5 @@ import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ItemModule } from "@bitwarden/components"; @@ -22,6 +23,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co PopupHeaderComponent, PopupPageComponent, PopupFooterComponent, + RouterModule, ItemModule, ], }) diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html index dffb3612c39..bcc0f12d0d7 100644 --- a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html +++ b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html @@ -9,7 +9,6 @@ diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index c857d952ab7..e27d80e595a 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -32,6 +32,7 @@ "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/vault": ["../../libs/vault/src"] }, diff --git a/apps/cli/package.json b/apps/cli/package.json index 502e186d346..a609224dcb5 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.48", + "tldts": "6.1.51", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 7643683221a..8f8f1fa4563 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -858,7 +858,7 @@ export class ServiceContainer { } if (!supported) { - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } } } diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml index 681c34c8eab..6f8005811d6 100644 --- a/apps/desktop/desktop_native/proxy/Cargo.toml +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -10,7 +10,7 @@ publish = false anyhow = "=1.0.86" desktop_core = { path = "../core", default-features = false } futures = "0.3.30" -log = "0.4.21" +log = "0.4.22" simplelog = "0.12.2" tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] } tokio-util = { version = "0.7.11", features = ["codec"] } diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 5dfe6e5a917..b243b51acc9 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -18,7 +18,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.10", + "@types/node": "20.16.11", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,9 +106,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", - "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 0dc8c461295..ccd480e31b6 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -23,7 +23,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.10", + "@types/node": "20.16.11", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 86a39163f3a..e8ae31e78a8 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -141,8 +141,12 @@ const routes: Routes = [ path: "hint", canActivate: [unauthGuardFn()], data: { - pageTitle: "requestPasswordHint", - pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, pageIcon: UserLockIcon, } satisfies AnonLayoutWrapperData, children: [ @@ -164,7 +168,11 @@ const routes: Routes = [ { path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], - data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData, + data: { + pageTitle: { + key: "createAccount", + }, + } satisfies AnonLayoutWrapperData, children: [ { path: "", @@ -184,8 +192,12 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: "setAStrongPassword", - pageSubtitle: "finishCreatingYourAccountBySettingAPassword", + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, } satisfies AnonLayoutWrapperData, children: [ { @@ -199,7 +211,9 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], data: { pageIcon: LockIcon, - pageTitle: "yourVaultIsLockedV2", + pageTitle: { + key: "yourVaultIsLockedV2", + }, showReadonlyHostname: true, } satisfies AnonLayoutWrapperData, children: [ @@ -214,8 +228,12 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { - pageTitle: "joinOrganization", - pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword", + pageTitle: { + key: "joinOrganization", + }, + pageSubtitle: { + key: "finishJoiningThisOrganizationBySettingAMasterPassword", + }, } satisfies AnonLayoutWrapperData, }, ], diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index d3b39218b52..dceda128c85 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -167,7 +167,7 @@ export class AppComponent implements OnInit, OnDestroy { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 075ae5d7295..4647853f715 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1752,6 +1752,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index fe2c669dbc2..5fa0db4d80b 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -32,6 +32,7 @@ "@bitwarden/node/*": ["../../libs/node/src/*"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/vault": ["../../libs/vault/src"] }, "useDefineForClassFields": false diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index e6cd30caee9..7cefdd2165d 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -105,7 +105,7 @@ export class AppComponent implements OnDestroy, OnInit { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts index 2c58d614c51..0b5c4f5e437 100644 --- a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts @@ -14,22 +14,24 @@ describe("freeTrialTextResolver", () => { it("shows password manager text", () => { route.queryParams.product = `${ProductType.PasswordManager}`; - expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe( - "continueSettingUpFreeTrialPasswordManager", - ); + expect(freeTrialTextResolver(route, routerStateSnapshot)).toEqual({ + key: "continueSettingUpFreeTrialPasswordManager", + }); }); it("shows secret manager text", () => { route.queryParams.product = `${ProductType.SecretsManager}`; - expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe( - "continueSettingUpFreeTrialSecretsManager", - ); + expect(freeTrialTextResolver(route, routerStateSnapshot)).toEqual({ + key: "continueSettingUpFreeTrialSecretsManager", + }); }); it("shows default text", () => { route.queryParams.product = `${ProductType.PasswordManager},${ProductType.SecretsManager}`; - expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe("continueSettingUpFreeTrial"); + expect(freeTrialTextResolver(route, routerStateSnapshot)).toEqual({ + key: "continueSettingUpFreeTrial", + }); }); }); diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts index cd23ef5ff7e..28d61a6de13 100644 --- a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts @@ -1,10 +1,11 @@ import { ActivatedRouteSnapshot, ResolveFn } from "@angular/router"; import { ProductType } from "@bitwarden/common/billing/enums"; +import { Translation } from "@bitwarden/components"; -export const freeTrialTextResolver: ResolveFn = ( +export const freeTrialTextResolver: ResolveFn = ( route: ActivatedRouteSnapshot, -): string | null => { +): Translation | null => { const { product } = route.queryParams; const products: ProductType[] = (product ?? "").split(",").map((p: string) => parseInt(p)); @@ -13,10 +14,16 @@ export const freeTrialTextResolver: ResolveFn = ( switch (true) { case onlyPasswordManager: - return "continueSettingUpFreeTrialPasswordManager"; + return { + key: "continueSettingUpFreeTrialPasswordManager", + }; case onlySecretsManager: - return "continueSettingUpFreeTrialSecretsManager"; + return { + key: "continueSettingUpFreeTrialSecretsManager", + }; default: - return "continueSettingUpFreeTrial"; + return { + key: "continueSettingUpFreeTrial", + }; } }; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 18b9a301510..e7ae154ec4e 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -228,7 +228,9 @@ const routes: Routes = [ path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: "createAccount", + pageTitle: { + key: "createAccount", + }, titleId: "createAccount", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -250,8 +252,12 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: "setAStrongPassword", - pageSubtitle: "finishCreatingYourAccountBySettingAPassword", + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, titleId: "setAStrongPassword", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -264,7 +270,9 @@ const routes: Routes = [ { path: "send/:sendId/:key", data: { - pageTitle: "viewSend", + pageTitle: { + key: "viewSend", + }, showReadonlyHostname: true, } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -284,15 +292,21 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { - pageTitle: "joinOrganization", - pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword", + pageTitle: { + key: "joinOrganization", + }, + pageSubtitle: { + key: "finishJoiningThisOrganizationBySettingAMasterPassword", + }, } satisfies AnonLayoutWrapperData, }, { path: "signup-link-expired", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: "expiredLink", + pageTitle: { + key: "expiredLink", + }, } satisfies AnonLayoutWrapperData, children: [ { @@ -308,7 +322,9 @@ const routes: Routes = [ path: "sso", canActivate: [unauthGuardFn()], data: { - pageTitle: "enterpriseSingleSignOn", + pageTitle: { + key: "enterpriseSingleSignOn", + }, titleId: "enterpriseSingleSignOn", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -338,7 +354,9 @@ const routes: Routes = [ }, ], data: { - pageTitle: "logIn", + pageTitle: { + key: "logIn", + }, }, }, ...extensionRefreshSwap( @@ -354,7 +372,9 @@ const routes: Routes = [ }, ], data: { - pageTitle: "yourVaultIsLockedV2", + pageTitle: { + key: "yourVaultIsLockedV2", + }, pageIcon: LockIcon, showReadonlyHostname: true, } satisfies AnonLayoutWrapperData, @@ -369,7 +389,9 @@ const routes: Routes = [ }, ], data: { - pageTitle: "yourAccountIsLocked", + pageTitle: { + key: "yourAccountIsLocked", + }, pageIcon: LockIcon, showReadonlyHostname: true, } satisfies AnonLayoutWrapperData, @@ -390,7 +412,9 @@ const routes: Routes = [ }, ], data: { - pageTitle: "verifyIdentity", + pageTitle: { + key: "verifyIdentity", + }, } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { @@ -408,7 +432,9 @@ const routes: Routes = [ }, ], data: { - pageTitle: "recoverAccountTwoStep", + pageTitle: { + key: "recoverAccountTwoStep", + }, titleId: "recoverAccountTwoStep", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, @@ -416,7 +442,9 @@ const routes: Routes = [ path: "accept-emergency", canActivate: [deepLinkGuard()], data: { - pageTitle: "emergencyAccess", + pageTitle: { + key: "emergencyAccess", + }, titleId: "acceptEmergency", doNotSaveUrl: false, } satisfies RouteDataProperties & AnonLayoutWrapperData, @@ -434,7 +462,9 @@ const routes: Routes = [ path: "recover-delete", canActivate: [unauthGuardFn()], data: { - pageTitle: "deleteAccount", + pageTitle: { + key: "deleteAccount", + }, titleId: "deleteAccount", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -453,7 +483,9 @@ const routes: Routes = [ path: "verify-recover-delete", canActivate: [unauthGuardFn()], data: { - pageTitle: "deleteAccount", + pageTitle: { + key: "deleteAccount", + }, titleId: "deleteAccount", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -468,7 +500,9 @@ const routes: Routes = [ component: RemovePasswordComponent, canActivate: [authGuard], data: { - pageTitle: "removeMasterPassword", + pageTitle: { + key: "removeMasterPassword", + }, titleId: "removeMasterPassword", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index 07697ee8b37..32bd554baa8 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -157,8 +157,8 @@ export class AccessComponent implements OnInit { if (this.creatorIdentifier != null) { this.layoutWrapperDataService.setAnonLayoutWrapperData({ pageSubtitle: { - subtitle: this.i18nService.t("sendAccessCreatorIdentifier", this.creatorIdentifier), - translate: false, + key: "sendAccessCreatorIdentifier", + placeholders: [this.creatorIdentifier], }, }); } diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 4302edcb4ec..2f38d7c70db 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -1,5 +1,6 @@ (); protected CipherType = CipherType; + protected organization?: Organization; constructor(private configService: ConfigService) {} @@ -53,6 +54,9 @@ export class VaultCipherRowComponent implements OnInit { this.extensionRefreshEnabled = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), ); + if (this.cipher.organizationId != null) { + this.organization = this.organizations.find((o) => o.id === this.cipher.organizationId); + } } protected get showTotpCopyButton() { @@ -138,4 +142,12 @@ export class VaultCipherRowComponent implements OnInit { protected assignToCollections() { this.onEvent.emit({ type: "assignToCollections", items: [this.cipher] }); } + + protected get showCheckbox() { + if (!this.viewingOrgVault || !this.organization) { + return true; // Always show checkbox in individual vault or for non-org items + } + + return this.organization.canEditAllCiphers || this.cipher.edit; + } } diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 36cd3679a0d..9656e4e8353 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -103,6 +103,10 @@ export class VaultCollectionRowComponent { } protected get showCheckbox() { - return this.collection?.id !== Unassigned; + if (this.collection?.id === Unassigned) { + return false; // Never show checkbox for Unassigned + } + + return this.canEditCollection || this.canDeleteCollection; } } diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index ba458b5c3a9..d3539139076 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { Unassigned, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView, Unassigned } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { TableDataSource } from "@bitwarden/components"; @@ -205,11 +205,12 @@ export class VaultItemsComponent { this.selection.clear(); - // Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission + // All ciphers are selectable, collections only if they can be edited or deleted this.editableItems = items.filter( (item) => item.cipher !== undefined || - (item.collection !== undefined && item.collection.id !== Unassigned), + (item.collection !== undefined && + (this.canEditCollection(item.collection) || this.canDeleteCollection(item.collection))), ); this.dataSource.data = items; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 61894f29883..5c876e0a8a4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7922,6 +7922,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "notFound": { "message": "$RESOURCE$ not found", "placeholders": { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 1e17de148f0..3b0c897e91e 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -26,6 +26,7 @@ "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/web-vault/*": ["src/*"] } diff --git a/bitwarden_license/bit-common/tsconfig.json b/bitwarden_license/bit-common/tsconfig.json index 85ba8cbf60b..03f3bd2d2f1 100644 --- a/bitwarden_license/bit-common/tsconfig.json +++ b/bitwarden_license/bit-common/tsconfig.json @@ -21,6 +21,7 @@ ], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/vault": ["../../libs/vault/src"], diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 55675f2186d..50db2cb6a36 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -22,7 +22,7 @@ - + {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ i.cadence.toLowerCase() diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index 43521ebe863..1ea888d6ebc 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -101,4 +101,14 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { this.destroy$.next(); this.destroy$.complete(); } + + get activePlans(): ProviderPlanResponse[] { + return this.subscription.plans.filter((plan) => { + if (plan.purchasedSeats === 0) { + return plan.seatMinimum > 0; + } else { + return plan.purchasedSeats > 0; + } + }); + } } diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 968744d7963..3ccdade273e 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -26,6 +26,7 @@ "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/web-vault/*": ["../../apps/web/src/*"], diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg index bc0a348fee4..606f39e1168 100644 --- a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg +++ b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg @@ -38,13 +38,13 @@ - + - + @@ -52,16 +52,16 @@ - + - - - + + + @@ -107,7 +107,7 @@ - + @@ -125,7 +125,7 @@ - + @@ -157,8 +157,8 @@ - - + + @@ -172,7 +172,7 @@ - + @@ -191,13 +191,32 @@ - + - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf index f70eea7af77..523c5233e03 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf and b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff index 52cecc3ead8..4eef2c86034 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 index 4c8cfd6e047..7353bb99ef5 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 differ diff --git a/libs/angular/src/scss/bwicons/styles/style.scss b/libs/angular/src/scss/bwicons/styles/style.scss index e1333da468f..3a0c9610bd4 100644 --- a/libs/angular/src/scss/bwicons/styles/style.scss +++ b/libs/angular/src/scss/bwicons/styles/style.scss @@ -239,11 +239,15 @@ $icons: ( "github": "\e950", "facebook": "\e94d", "paypal": "\e938", - "google": "\e951", + "brave": "\e951", + "google": "\e9a5", + "duckduckgo": "\e9bb", + "tor": "\e9bc", + "vivaldi": "\e9bd", "linkedin": "\e955", "discourse": "\e91e", "twitter": "\e961", - "x-twitter": "\e9a5", + "x-twitter": "\e9be", "youtube": "\e966", "windows": "\e964", "apple": "\e945", @@ -276,6 +280,21 @@ $icons: ( "popout": "\e9aa", "wand": "\e9a6", "msp": "\e9a1", + "totp-codes-alt": "\e9ac", + "totp-codes-alt2": "\e9ad", + "totp-codes": "\e9ae", + "authenticator": "\e9af", + "fingerprint": "\e9b0", + "expired": "\e9ba", + "icon-1": "\e9b1", + "icon-2": "\e9b2", + "icon-3": "\e9b3", + "icon-4": "\e9b4", + "icon-5": "\e9b5", + "icon-6": "\e9b6", + "icon-7": "\e9b7", + "icon-8": "\e9b8", + "icon-9": "\e9b9", ); @each $name, $glyph in $icons { diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index 5272342da1d..27446335740 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -4,20 +4,34 @@ import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; import { AnonLayoutComponent } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Icon } from "@bitwarden/components"; +import { Icon, Translation } from "@bitwarden/components"; import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service"; export interface AnonLayoutWrapperData { - pageTitle?: string; - pageSubtitle?: - | string - | { - subtitle: string; - translate: boolean; - }; + /** + * The optional title of the page. + * If a string is provided, it will be presented as is (ex: Organization name) + * If a Translation object (supports placeholders) is provided, it will be translated + */ + pageTitle?: string | Translation; + /** + * The optional subtitle of the page. + * If a string is provided, it will be presented as is (ex: user's email) + * If a Translation object (supports placeholders) is provided, it will be translated + */ + pageSubtitle?: string | Translation; + /** + * The optional icon to display on the page. + */ pageIcon?: Icon; + /** + * Optional flag to either show the optional environment selector (false) or just a readonly hostname (true). + */ showReadonlyHostname?: boolean; + /** + * Optional flag to set the max-width of the page. Defaults to 'md' if not provided. + */ maxWidth?: "md" | "3xl"; } @@ -71,11 +85,11 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { } if (firstChildRouteData["pageTitle"] !== undefined) { - this.pageTitle = this.i18nService.t(firstChildRouteData["pageTitle"]); + this.pageTitle = this.handleStringOrTranslation(firstChildRouteData["pageTitle"]); } if (firstChildRouteData["pageSubtitle"] !== undefined) { - this.pageSubtitle = this.i18nService.t(firstChildRouteData["pageSubtitle"]); + this.pageSubtitle = this.handleStringOrTranslation(firstChildRouteData["pageSubtitle"]); } if (firstChildRouteData["pageIcon"] !== undefined) { @@ -101,19 +115,11 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { } if (data.pageTitle) { - this.pageTitle = this.i18nService.t(data.pageTitle); + this.pageTitle = this.handleStringOrTranslation(data.pageTitle); } if (data.pageSubtitle) { - // If you pass just a string, we translate it by default - if (typeof data.pageSubtitle === "string") { - this.pageSubtitle = this.i18nService.t(data.pageSubtitle); - } else { - // if you pass an object, you can specify if you want to translate it or not - this.pageSubtitle = data.pageSubtitle.translate - ? this.i18nService.t(data.pageSubtitle.subtitle) - : data.pageSubtitle.subtitle; - } + this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle); } if (data.pageIcon) { @@ -129,6 +135,16 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.changeDetectorRef.detectChanges(); } + private handleStringOrTranslation(value: string | Translation): string { + if (typeof value === "string") { + // If it's a string, return it as is + return value; + } + + // If it's a Translation object, translate it + return this.i18nService.t(value.key, ...(value.placeholders ?? [])); + } + private resetPageData() { this.pageTitle = null; this.pageSubtitle = null; diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts index 87e26bd2df1..b07504b7c8d 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts @@ -163,17 +163,20 @@ export const DefaultContentExample: Story = { // Dynamic Content Example const initialData: AnonLayoutWrapperData = { - pageTitle: "setAStrongPassword", - pageSubtitle: "finishCreatingYourAccountBySettingAPassword", + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, pageIcon: LockIcon, }; const changedData: AnonLayoutWrapperData = { - pageTitle: "enterpriseSingleSignOn", - pageSubtitle: { - subtitle: "user@email.com (non-translated)", - translate: false, + pageTitle: { + key: "enterpriseSingleSignOn", }, + pageSubtitle: "user@email.com (non-translated)", pageIcon: RegistrationCheckEmailIcon, }; diff --git a/libs/auth/src/angular/lock/lock.component.ts b/libs/auth/src/angular/lock/lock.component.ts index 7bea14f221e..33d318ac058 100644 --- a/libs/auth/src/angular/lock/lock.component.ts +++ b/libs/auth/src/angular/lock/lock.component.ts @@ -233,10 +233,7 @@ export class LockV2Component implements OnInit, OnDestroy { private setEmailAsPageSubtitle(email: string) { this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ - pageSubtitle: { - subtitle: email, - translate: false, - }, + pageSubtitle: email, }); } diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index 4ccec81a447..9333fa23368 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -58,6 +58,8 @@ export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event"; +export const AUTOFILL_OVERLAY_HANDLE_SCROLL = "autofill-overlay-handle-scroll-event"; + export const UPDATE_PASSKEYS_HEADINGS_ON_SCROLL = "update-passkeys-headings-on-scroll"; export const AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT = "autofill-trigger-form-field-submit"; diff --git a/libs/common/src/autofill/services/autofill-settings.service.ts b/libs/common/src/autofill/services/autofill-settings.service.ts index 123f69550c3..09fdde8997b 100644 --- a/libs/common/src/autofill/services/autofill-settings.service.ts +++ b/libs/common/src/autofill/services/autofill-settings.service.ts @@ -59,6 +59,24 @@ const INLINE_MENU_VISIBILITY = new KeyDefinition( }, ); +const SHOW_INLINE_MENU_IDENTITIES = new UserKeyDefinition( + AUTOFILL_SETTINGS_DISK, + "showInlineMenuIdentities", + { + deserializer: (value: boolean) => value ?? true, + clearOn: [], + }, +); + +const SHOW_INLINE_MENU_CARDS = new UserKeyDefinition( + AUTOFILL_SETTINGS_DISK, + "showInlineMenuCards", + { + deserializer: (value: boolean) => value ?? true, + clearOn: [], + }, +); + const ENABLE_CONTEXT_MENU = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "enableContextMenu", { deserializer: (value: boolean) => value ?? true, }); @@ -86,6 +104,10 @@ export abstract class AutofillSettingsServiceAbstraction { setAutoCopyTotp: (newValue: boolean) => Promise; inlineMenuVisibility$: Observable; setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise; + showInlineMenuIdentities$: Observable; + setShowInlineMenuIdentities: (newValue: boolean) => Promise; + showInlineMenuCards$: Observable; + setShowInlineMenuCards: (newValue: boolean) => Promise; enableContextMenu$: Observable; setEnableContextMenu: (newValue: boolean) => Promise; clearClipboardDelay$: Observable; @@ -113,6 +135,12 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti private inlineMenuVisibilityState: GlobalState; readonly inlineMenuVisibility$: Observable; + private showInlineMenuIdentitiesState: ActiveUserState; + readonly showInlineMenuIdentities$: Observable; + + private showInlineMenuCardsState: ActiveUserState; + readonly showInlineMenuCards$: Observable; + private enableContextMenuState: GlobalState; readonly enableContextMenu$: Observable; @@ -157,6 +185,14 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti map((x) => x ?? AutofillOverlayVisibility.Off), ); + this.showInlineMenuIdentitiesState = this.stateProvider.getActive(SHOW_INLINE_MENU_IDENTITIES); + this.showInlineMenuIdentities$ = this.showInlineMenuIdentitiesState.state$.pipe( + map((x) => x ?? true), + ); + + this.showInlineMenuCardsState = this.stateProvider.getActive(SHOW_INLINE_MENU_CARDS); + this.showInlineMenuCards$ = this.showInlineMenuCardsState.state$.pipe(map((x) => x ?? true)); + this.enableContextMenuState = this.stateProvider.getGlobal(ENABLE_CONTEXT_MENU); this.enableContextMenu$ = this.enableContextMenuState.state$.pipe(map((x) => x ?? true)); @@ -190,6 +226,14 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti await this.inlineMenuVisibilityState.update(() => newValue); } + async setShowInlineMenuIdentities(newValue: boolean): Promise { + await this.showInlineMenuIdentitiesState.update(() => newValue); + } + + async setShowInlineMenuCards(newValue: boolean): Promise { + await this.showInlineMenuCardsState.update(() => newValue); + } + async setEnableContextMenu(newValue: boolean): Promise { await this.enableContextMenuState.update(() => newValue); } diff --git a/libs/components/src/stories/icons.mdx b/libs/components/src/stories/icons.mdx index 8bc3ad7f3ac..f16b5d56b16 100644 --- a/libs/components/src/stories/icons.mdx +++ b/libs/components/src/stories/icons.mdx @@ -17,6 +17,7 @@ or an options menu icon. | | bwi-ban | option or feature not available. Example: send maximum access count was reached | | | bwi-check | confirmation action (Example: "confirm member"), successful confirmation (toast or callout), or shows currently selected option in a menu. Use with success color variable if applicable. | | | bwi-error | error; used in form field error states and error toasts, banners, and callouts. Do not use as a close or clear icon. Use with danger color variable. | +| | bwi-expired | - | | | bwi-exclamation-circle | deprecated error icon; use bwi-error | | | bwi-exclamation-triangle | warning; used in warning callouts, banners, and toasts. Use with warning color variable. | | | bwi-info-circle | information; used in info callouts, banners, and toasts. Use with info color variable. | @@ -25,21 +26,22 @@ or an options menu icon. ## Bitwarden Objects -| Icon | bwi-name | Usage | -| ----------------------------------- | --------------- | --------------------------------------------------- | -| | bwi-business | organization or vault for Free, Teams or Enterprise | -| | bwi-collection | collection | -| | bwi-credit-card | card item type | -| | bwi-family | family vault or organization | -| | bwi-folder | folder | -| | bwi-globe | login item type | -| | bwi-id-card | identity item type | -| | bwi-send | send action or feature | -| | bwi-send-f | - | -| | bwi-sticky-note | secure note item type | -| | bwi-users | user group | -| | bwi-vault | general vault | -| | bwi-vault-f | general vault | +| Icon | bwi-name | Usage | +| ------------------------------------- | ----------------- | --------------------------------------------------- | +| | bwi-authenticator | authenticator app | +| | bwi-business | organization or vault for Free, Teams or Enterprise | +| | bwi-collection | collection | +| | bwi-credit-card | card item type | +| | bwi-family | family vault or organization | +| | bwi-folder | folder | +| | bwi-globe | login item type | +| | bwi-id-card | identity item type | +| | bwi-send | send action or feature | +| | bwi-send-f | - | +| | bwi-sticky-note | secure note item type | +| | bwi-users | user group | +| | bwi-vault | general vault | +| | bwi-vault-f | general vault | ## Actions @@ -146,11 +148,21 @@ or an options menu icon. | | bwi-file | file related objects or actions | | | bwi-file-pdf | PDF related object or actions | | | bwi-file-text | text related objects or actions | +| | bwi-fingerprint | - | | | bwi-bw-folder-open-f1 | - | | | bwi-folder-closed-f | - | | | bwi-folder-open | - | | | bwi-frown | - | | | bwi-hashtag | link to specific id | +| | bwi-icon-1 | - | +| | bwi-icon-2 | - | +| | bwi-icon-3 | - | +| | bwi-icon-4 | - | +| | bwi-icon-5 | - | +| | bwi-icon-6 | - | +| | bwi-icon-7 | - | +| | bwi-icon-8 | - | +| | bwi-icon-9 | - | | | bwi-insurance | - | | | bwi-key | key or password related objects or actions | | | bwi-learning | learning center | @@ -178,6 +190,9 @@ or an options menu icon. | | bwi-tag | labels | | | bwi-thumb-tack | - | | | bwi-thumbs-up | - | +| | bwi-totp-codes | - | +| | bwi-totp-codes-alt | - | +| | bwi-totp-codes-alt2 | - | | | bwi-universal-access | use for accessibility related actions | | | bwi-user | relates to current user or organization member | | | bwi-user-circle | - | @@ -189,27 +204,31 @@ or an options menu icon. ## Platforms and Logos -| Icon | bwi-name | Usage | -| --------------------------------- | ------------- | ---------------------------- | -| | bwi-android | android support | -| | bwi-apple | apple/IOS support | -| | bwi-chrome | chrome support | -| | bwi-discourse | community forum | -| | bwi-edge | edge support | -| | bwi-facebook | link to our facebook page | -| | bwi-firefox | support for firefox | -| | bwi-github | link to our github page | -| | bwi-google | link to our google page | -| | bwi-instagram | link to our Instagram page | -| | bwi-linkedin | link to our linkedIn page | -| | bwi-linux | linux support | -| | bwi-mastodon | link to our Mastodon page | -| | bwi-opera | support for Opera | -| | bwi-paypal | PayPal | -| | bwi-reddit | link to our reddit community | -| | bwi-safari | safari support | -| | bwi-twitch | link to our Twitch page | -| | bwi-twitter | link to our twitter page | -| | bwi-windows | support for windows | -| | bwi-x-twitter | x version of twitter | -| | bwi-youtube | link to our youtube page | +| Icon | bwi-name | Usage | +| ---------------------------------- | -------------- | ---------------------------- | +| | bwi-android | android support | +| | bwi-apple | apple/IOS support | +| | bwi-brave | - | +| | bwi-chrome | chrome support | +| | bwi-discourse | community forum | +| | bwi-duckduckgo | - | +| | bwi-edge | edge support | +| | bwi-facebook | link to our facebook page | +| | bwi-firefox | support for firefox | +| | bwi-github | link to our github page | +| | bwi-google | link to our google page | +| | bwi-instagram | link to our Instagram page | +| | bwi-linkedin | link to our linkedIn page | +| | bwi-linux | linux support | +| | bwi-mastodon | link to our Mastodon page | +| | bwi-opera | support for Opera | +| | bwi-paypal | PayPal | +| | bwi-reddit | link to our reddit community | +| | bwi-safari | safari support | +| | bwi-twitch | link to our Twitch page | +| | bwi-twitter | link to our twitter page | +| | bwi-tor | - | +| | bwi-vivaldi | - | +| | bwi-windows | support for windows | +| | bwi-x-twitter | x version of twitter | +| | bwi-youtube | link to our youtube page | diff --git a/libs/shared/tsconfig.libs.json b/libs/shared/tsconfig.libs.json index 312a04f3b03..6057152419c 100644 --- a/libs/shared/tsconfig.libs.json +++ b/libs/shared/tsconfig.libs.json @@ -22,6 +22,7 @@ "@bitwarden/key-management": ["../key-management/src"], "@bitwarden/platform": ["../platform/src"], "@bitwarden/send-ui": ["../tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../tools/card/src"], "@bitwarden/node/*": ["../node/src/*"], "@bitwarden/vault": ["../vault/src"] } diff --git a/libs/tools/card/README.md b/libs/tools/card/README.md new file mode 100644 index 00000000000..5e28e62d154 --- /dev/null +++ b/libs/tools/card/README.md @@ -0,0 +1,5 @@ +## Tools Card + +Package name: `@bitwarden/tools-card` + +Generic Tools Card Component diff --git a/libs/tools/card/jest.config.js b/libs/tools/card/jest.config.js new file mode 100644 index 00000000000..b68bda8d5ca --- /dev/null +++ b/libs/tools/card/jest.config.js @@ -0,0 +1,13 @@ +const { pathsToModuleNameMapper } = require("ts-jest"); + +const { compilerOptions } = require("../../../shared/tsconfig.libs"); + +/** @type {import('jest').Config} */ +module.exports = { + testMatch: ["**/+(*.)+(spec).+(ts)"], + preset: "jest-preset-angular", + setupFilesAfterEnv: ["/test.setup.ts"], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/../../", + }), +}; diff --git a/libs/tools/card/package.json b/libs/tools/card/package.json new file mode 100644 index 00000000000..904429ba745 --- /dev/null +++ b/libs/tools/card/package.json @@ -0,0 +1,24 @@ +{ + "name": "@bitwarden/tools-card", + "version": "0.0.0", + "description": "Angular card component", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "build:watch": "npm run clean && tsc -watch" + }, + "dependencies": { + "@bitwarden/common": "file:../../../common", + "@bitwarden/components": "file:../../../components" + } +} diff --git a/libs/tools/card/src/card.component.html b/libs/tools/card/src/card.component.html new file mode 100644 index 00000000000..3fd9372087c --- /dev/null +++ b/libs/tools/card/src/card.component.html @@ -0,0 +1,7 @@ +
+ {{ title }} +
+ {{ value }} + {{ "cardMetrics" | i18n: maxValue }} +
+
diff --git a/libs/tools/card/src/card.component.ts b/libs/tools/card/src/card.component.ts new file mode 100644 index 00000000000..9305246c581 --- /dev/null +++ b/libs/tools/card/src/card.component.ts @@ -0,0 +1,30 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { TypographyModule } from "@bitwarden/components"; + +@Component({ + selector: "tools-card", + templateUrl: "./card.component.html", + standalone: true, + imports: [CommonModule, TypographyModule, JslibModule], + host: { + class: + "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-p-6", + }, +}) +export class CardComponent { + /** + * The title of the card + */ + @Input() title: string; + /** + * The current value of the card as emphasized text + */ + @Input() value: number; + /** + * The maximum value of the card + */ + @Input() maxValue: number; +} diff --git a/libs/tools/card/src/card.stories.ts b/libs/tools/card/src/card.stories.ts new file mode 100644 index 00000000000..94a794dba12 --- /dev/null +++ b/libs/tools/card/src/card.stories.ts @@ -0,0 +1,36 @@ +import { CommonModule } from "@angular/common"; +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nMockService, TypographyModule } from "@bitwarden/components"; + +import { CardComponent } from "./card.component"; + +export default { + title: "Toools/Card", + component: CardComponent, + decorators: [ + moduleMetadata({ + imports: [CardComponent, CommonModule, TypographyModule], + providers: [ + { + provide: I18nService, + useFactory: () => + new I18nMockService({ + cardMetrics: (value) => `out of ${value}`, + }), + }, + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + `, + }), +}; diff --git a/libs/tools/card/src/index.ts b/libs/tools/card/src/index.ts new file mode 100644 index 00000000000..d88b9db709e --- /dev/null +++ b/libs/tools/card/src/index.ts @@ -0,0 +1 @@ +export { CardComponent } from "./card.component"; diff --git a/libs/tools/card/test.setup.ts b/libs/tools/card/test.setup.ts new file mode 100644 index 00000000000..a702c633967 --- /dev/null +++ b/libs/tools/card/test.setup.ts @@ -0,0 +1 @@ +import "jest-preset-angular/setup-jest"; diff --git a/libs/tools/card/tsconfig.json b/libs/tools/card/tsconfig.json new file mode 100644 index 00000000000..52eed3035a6 --- /dev/null +++ b/libs/tools/card/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../shared/tsconfig.libs", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/tools/card/tsconfig.spec.json b/libs/tools/card/tsconfig.spec.json new file mode 100644 index 00000000000..919530506de --- /dev/null +++ b/libs/tools/card/tsconfig.spec.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "files": ["./test.setup.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 91a7c12210d..b174349ecef 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -15,7 +15,7 @@
-
+
diff --git a/libs/tools/generator/components/src/generator.module.ts b/libs/tools/generator/components/src/generator.module.ts index c7dfc60bab2..96622774a3f 100644 --- a/libs/tools/generator/components/src/generator.module.ts +++ b/libs/tools/generator/components/src/generator.module.ts @@ -20,6 +20,7 @@ import { SectionHeaderComponent, SelectModule, ToggleGroupModule, + TypographyModule, } from "@bitwarden/components"; import { createRandomizer, @@ -55,6 +56,7 @@ const RANDOMIZER = new SafeInjectionToken("Randomizer"); SectionHeaderComponent, SelectModule, ToggleGroupModule, + TypographyModule, ], providers: [ safeProvider({ diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 7ec3a565dd3..9a33aa143ec 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -13,7 +13,7 @@
-
+
diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index f65b35a8420..fcafc789049 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -1,6 +1,6 @@ -
{{ "options" | i18n }}
+

{{ "options" | i18n }}

@@ -55,7 +55,7 @@
- + {{ "minNumbers" | i18n }} - + {{ "minSpecial" | i18n }}
-
+
diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index 9699f832ed0..cc1400f0a6c 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -12,8 +12,8 @@ > - {{ "password" | i18n }} - {{ "newPassword" | i18n }} + {{ "password" | i18n }} + {{ "newPassword" | i18n }}