diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 432479aa80a..491c04cc414 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1343,8 +1343,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1650,6 +1656,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -2774,14 +2784,17 @@ "autofillKeyboardShortcutUpdateLabel": { "message": "Change shortcut" }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The autofill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The autofill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index f1bfd3642fa..401196b256f 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -190,7 +190,6 @@ export type OverlayBackgroundExtensionMessageHandlers = { }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void; - doFullSync: () => void; addedCipher: () => void; addEditCipherSubmitted: () => void; editedCipher: () => void; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 9c2e63c1aa7..111871d57dc 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject, firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -151,7 +152,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "unlockCompleted", data: { - commandToRetry: { message: { command: "autofill_login" } }, + commandToRetry: { message: { command: ExtensionCommand.AutofillLogin } }, } as LockedVaultPendingNotificationsData, }; jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9e989b73e62..a047a3533a0 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -4,7 +4,11 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants"; +import { + ExtensionCommand, + ExtensionCommandType, + NOTIFICATION_BAR_LIFESPAN_MS, +} from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; @@ -45,6 +49,11 @@ export default class NotificationBackground { private openUnlockPopout = openUnlockPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout; private notificationQueue: NotificationQueueMessageItem[] = []; + private allowedRetryCommands: Set = new Set([ + ExtensionCommand.AutofillLogin, + ExtensionCommand.AutofillCard, + ExtensionCommand.AutofillIdentity, + ]); private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), bgGetFolderData: () => this.getFolderData(), @@ -689,8 +698,8 @@ export default class NotificationBackground { sender: chrome.runtime.MessageSender, ): Promise { const messageData = message.data as LockedVaultPendingNotificationsData; - const retryCommand = messageData.commandToRetry.message.command; - if (retryCommand === "autofill_login") { + const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType; + if (this.allowedRetryCommands.has(retryCommand)) { await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); } diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 3a3bb7dd5e8..b3c86992067 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -2014,7 +2014,6 @@ describe("OverlayBackground", () => { describe("extension messages that trigger an update of the inline menu ciphers", () => { const extensionMessages = [ - "doFullSync", "addedCipher", "addEditCipherSubmitted", "editedCipher", diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index eea72979dd2..029d2d69ac6 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -120,7 +120,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), - doFullSync: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), @@ -273,7 +272,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "") ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); - return cipherViews.concat(...this.cardAndIdentityCiphers); + return this.cardAndIdentityCiphers + ? cipherViews.concat(...this.cardAndIdentityCiphers) + : cipherViews; } /** diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index cb1c59dca59..c1567b46cd9 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -16,6 +16,7 @@ import { CREATE_CARD_ID, CREATE_IDENTITY_ID, CREATE_LOGIN_ID, + ExtensionCommand, GENERATE_PASSWORD_ID, NOOP_COMMAND_SUFFIX, } from "@bitwarden/common/autofill/constants"; @@ -79,7 +80,7 @@ export class ContextMenuClickedHandler { if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { const retryMessage: LockedVaultPendingNotificationsData = { commandToRetry: { - message: { command: NOOP_COMMAND_SUFFIX, contextMenuOnClickData: info }, + message: { command: ExtensionCommand.NoopCommand, contextMenuOnClickData: info }, sender: { tab: tab }, }, target: "contextmenus.background", 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 616c883f188..688b327d296 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 @@ -399,6 +399,11 @@ describe("AutofillInlineMenuContentService", () => { }); it("sets the z-index of to a lower value", async () => { + autofillInlineMenuContentService["handlePersistentLastChildOverrideTimeout"] = setTimeout( + jest.fn(), + 1000, + ); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); await waitForIdleCallback(); @@ -411,8 +416,9 @@ describe("AutofillInlineMenuContentService", () => { }); globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); - await waitForIdleCallback(); + await autofillInlineMenuContentService["verifyInlineMenuIsNotObscured"]( + persistentLastChild, + ); expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", { overlayElement: AutofillOverlayElement.Button, @@ -425,8 +431,9 @@ describe("AutofillInlineMenuContentService", () => { }); globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); - await waitForIdleCallback(); + await autofillInlineMenuContentService["verifyInlineMenuIsNotObscured"]( + persistentLastChild, + ); expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", { overlayElement: AutofillOverlayElement.List, 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 ae947415912..02d3ae052cc 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 @@ -33,6 +33,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private bodyElementMutationObserver: MutationObserver; private mutationObserverIterations = 0; private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; + private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout; private lastElementOverrides: WeakMap = new WeakMap(); private readonly customElementDefaultStyles: Partial = { all: "initial", @@ -405,7 +406,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } if (this.lastElementOverrides.get(lastChild) >= 3) { - await this.handlePersistentLastChildOverride(lastChild); + this.handlePersistentLastChildOverride(lastChild); return; } @@ -430,6 +431,26 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte globalThis.document.body.insertBefore(lastChild, this.buttonElement); }; + /** + * Handles the behavior of a persistent child element that is forcing itself to + * the bottom of the body element. This method will ensure that the inline menu + * elements are not obscured by the persistent child element. + * + * @param lastChild - The last child of the body element. + */ + private handlePersistentLastChildOverride(lastChild: Element) { + const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex); + if (lastChildZIndex >= 2147483647) { + (lastChild as HTMLElement).style.zIndex = "2147483646"; + } + + this.clearPersistentLastChildOverrideTimeout(); + this.handlePersistentLastChildOverrideTimeout = globalThis.setTimeout( + () => this.verifyInlineMenuIsNotObscured(lastChild), + 500, + ); + } + /** * Verifies if the last child of the body element is overlaying the inline menu elements. * This is triggered when the last child of the body is being forced by some script to @@ -437,12 +458,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * * @param lastChild - The last child of the body element. */ - private async handlePersistentLastChildOverride(lastChild: Element) { - const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex); - if (lastChildZIndex >= 2147483647) { - (lastChild as HTMLElement).style.zIndex = "2147483646"; - } - + private verifyInlineMenuIsNotObscured = async (lastChild: Element) => { const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage( "getAutofillInlineMenuPosition", ); @@ -456,7 +472,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte if (!!list && this.elementAtCenterOfInlineMenuPosition(list) === lastChild) { this.closeInlineMenu(); } - } + }; /** * Returns the element present at the center of the inline menu position. @@ -470,6 +486,16 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte ); } + /** + * Clears the timeout that is used to verify that the last child of the body element + * is not overlaying the inline menu elements. + */ + private clearPersistentLastChildOverrideTimeout() { + if (this.handlePersistentLastChildOverrideTimeout) { + globalThis.clearTimeout(this.handlePersistentLastChildOverrideTimeout); + } + } + /** * Identifies if the mutation observer is triggering excessive iterations. * Will trigger a blur of the most recently focused field and remove the @@ -503,5 +529,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte */ destroy() { this.closeInlineMenu(); + this.clearPersistentLastChildOverrideTimeout(); } } diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts index 7408e0d6fb8..f3f6f73cdb0 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts @@ -377,6 +377,21 @@ describe("AutofillInlineMenuIframeService", () => { autofillInlineMenuIframeService["ariaAlertElement"], ); }); + + it("resets the fade in timeout if it is set", () => { + autofillInlineMenuIframeService["fadeInTimeout"] = setTimeout(jest.fn, 100); + const styles = { top: "100px", left: "100px" }; + jest.spyOn(autofillInlineMenuIframeService as any, "handleFadeInInlineMenuIframe"); + + sendPortMessage(portSpy, { + command: "updateAutofillInlineMenuPosition", + styles, + }); + + expect( + autofillInlineMenuIframeService["handleFadeInInlineMenuIframe"], + ).toHaveBeenCalled(); + }); }); it("updates the visibility of the iframe", () => { diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts index fd305d23c9a..99519281bcf 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts @@ -260,10 +260,13 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe return; } + const styles = this.fadeInTimeout ? Object.assign(position, { opacity: "0" }) : position; + this.updateElementStyles(this.iframe, styles); + if (this.fadeInTimeout) { this.handleFadeInInlineMenuIframe(); } - this.updateElementStyles(this.iframe, position); + this.announceAriaAlert(); } @@ -320,10 +323,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe */ private handleFadeInInlineMenuIframe() { this.clearFadeInTimeout(); - this.fadeInTimeout = globalThis.setTimeout( - () => this.updateElementStyles(this.iframe, { display: "block", opacity: "1" }), - 10, - ); + this.fadeInTimeout = globalThis.setTimeout(() => { + this.updateElementStyles(this.iframe, { display: "block", opacity: "1" }); + this.clearFadeInTimeout(); + }, 10); } /** 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 261c6e459bf..8adee86bcf4 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts @@ -84,7 +84,7 @@ export class AutofillV1Component implements OnInit { { name: i18nService.t("fiveMinutes"), value: 300 }, ]; this.uriMatchOptions = [ - { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, + { name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain }, { name: i18nService.t("host"), value: UriMatchStrategy.Host }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, @@ -159,9 +159,9 @@ export class AutofillV1Component implements OnInit { private async setAutofillKeyboardHelperText(command: string) { if (command) { - this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command); + this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command); } else { - this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet"); + this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet"); } } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 5a7623f21c3..0933bc54217 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -8,7 +8,7 @@
-

{{ "autofillSuggestionsSectionTitle" | i18n }}

+

{{ "autofillSuggestionsSectionTitle" | i18n }}

@@ -87,7 +87,7 @@ /> {{ "showCardsInVaultView" | i18n }} - + -

{{ "autofillKeyboardShortcutSectionTitle" | i18n }}

+

{{ "autofillKeyboardShortcutSectionTitle" | i18n }}

+ + + `, +}) +class TestCopyClickComponent { + @ViewChild("noToast") noToastButton: ElementRef; + @ViewChild("infoToast") infoToastButton: ElementRef; + @ViewChild("successToast") successToastButton: ElementRef; +} + +describe("CopyClickDirective", () => { + let fixture: ComponentFixture; + const copyToClipboard = jest.fn(); + const showToast = jest.fn(); + + beforeEach(async () => { + copyToClipboard.mockClear(); + showToast.mockClear(); + + await TestBed.configureTestingModule({ + declarations: [CopyClickDirective, TestCopyClickComponent], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: ToastService, useValue: { showToast } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCopyClickComponent); + fixture.detectChanges(); + }); + + it("copies the the value for all variants of toasts ", () => { + const noToastButton = fixture.componentInstance.noToastButton.nativeElement; + + noToastButton.click(); + expect(copyToClipboard).toHaveBeenCalledWith("no toast shown"); + + const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement; + + infoToastButton.click(); + expect(copyToClipboard).toHaveBeenCalledWith("info toast shown"); + + const successToastButton = fixture.componentInstance.successToastButton.nativeElement; + + successToastButton.click(); + expect(copyToClipboard).toHaveBeenCalledWith("success toast shown"); + }); + + it("does not show a toast when showToast is not present", () => { + const noToastButton = fixture.componentInstance.noToastButton.nativeElement; + + noToastButton.click(); + expect(showToast).not.toHaveBeenCalled(); + }); + + it("shows a success toast when showToast is present", () => { + const successToastButton = fixture.componentInstance.successToastButton.nativeElement; + + successToastButton.click(); + expect(showToast).toHaveBeenCalledWith({ + message: "copySuccessful", + title: null, + variant: "success", + }); + }); + + it("shows the toast variant when set with showToast", () => { + const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement; + + infoToastButton.click(); + expect(showToast).toHaveBeenCalledWith({ + message: "copySuccessful", + title: null, + variant: "info", + }); + }); +}); diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/angular/src/directives/copy-click.directive.ts index cee2bdde4e8..0d764c95edb 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/angular/src/directives/copy-click.directive.ts @@ -1,14 +1,17 @@ -import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Directive, HostListener, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +import { ToastVariant } from "@bitwarden/components/src/toast/toast.component"; @Directive({ selector: "[appCopyClick]", }) export class CopyClickDirective { + private _showToast = false; + private toastVariant: ToastVariant = "success"; + constructor( private platformUtilsService: PlatformUtilsService, private toastService: ToastService, @@ -16,14 +19,36 @@ export class CopyClickDirective { ) {} @Input("appCopyClick") valueToCopy = ""; - @Input({ transform: coerceBooleanProperty }) showToast?: boolean; + + /** + * When set without a value, a success toast will be shown when the value is copied + * @example + * ```html + * + * ``` + * When set with a value, a toast with the specified variant will be shown when the value is copied + * + * @example + * ```html + * + * ``` + */ + @Input() set showToast(value: ToastVariant | "") { + // When the `showToast` is set without a value, an empty string will be passed + if (value === "") { + this._showToast = true; + } else { + this._showToast = true; + this.toastVariant = value; + } + } @HostListener("click") onClick() { this.platformUtilsService.copyToClipboard(this.valueToCopy); - if (this.showToast) { + if (this._showToast) { this.toastService.showToast({ - variant: "info", + variant: this.toastVariant, title: null, message: this.i18nService.t("copySuccessful"), }); diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index d3b5e6ee108..215998a560c 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -85,3 +85,17 @@ export const DisablePasswordManagerUris = { Vivaldi: "vivaldi://settings/autofill", Unknown: "https://bitwarden.com/help/disable-browser-autofill/", } as const; + +export const ExtensionCommand = { + AutofillCommand: "autofill_cmd", + AutofillCard: "autofill_card", + AutofillIdentity: "autofill_identity", + AutofillLogin: "autofill_login", + OpenAutofillOverlay: "open_autofill_overlay", + GeneratePassword: "generate_password", + OpenPopup: "open_popup", + LockVault: "lock_vault", + NoopCommand: "noop", +} as const; + +export type ExtensionCommandType = (typeof ExtensionCommand)[keyof typeof ExtensionCommand]; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 2c8af23ba3d..ad4a08c2fed 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -162,4 +162,6 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + getNextCardCipher: () => Promise; + getNextIdentityCipher: () => Promise; } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index aa1d3a18279..374f66c42e6 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -500,6 +500,13 @@ export class CipherService implements CipherServiceAbstraction { }); } + private async getAllDecryptedCiphersOfType(type: CipherType[]): Promise { + const ciphers = await this.getAllDecrypted(); + return ciphers + .filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type)) + .sort((a, b) => this.sortCiphersByLastUsedThenName(a, b)); + } + async getAllFromApiForOrganization(organizationId: string): Promise { const response = await this.apiService.getCiphersOrganization(organizationId); return await this.decryptOrganizationCiphersResponse(response, organizationId); @@ -549,6 +556,36 @@ export class CipherService implements CipherServiceAbstraction { return this.getCipherForUrl(url, false, false, false); } + async getNextCardCipher(): Promise { + const cacheKey = "cardCiphers"; + + if (!this.sortedCiphersCache.isCached(cacheKey)) { + const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card]); + if (!ciphers?.length) { + return null; + } + + this.sortedCiphersCache.addCiphers(cacheKey, ciphers); + } + + return this.sortedCiphersCache.getNext(cacheKey); + } + + async getNextIdentityCipher() { + const cacheKey = "identityCiphers"; + + if (!this.sortedCiphersCache.isCached(cacheKey)) { + const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity]); + if (!ciphers?.length) { + return null; + } + + this.sortedCiphersCache.addCiphers(cacheKey, ciphers); + } + + return this.sortedCiphersCache.getNext(cacheKey); + } + updateLastUsedIndexForUrl(url: string) { this.sortedCiphersCache.updateLastUsedIndex(url); }