From b66430b25cb68fe3f8a99b6f1d2631a3807a199e Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 15 Apr 2025 16:36:05 -0400 Subject: [PATCH 01/45] [PM-19781] Lit Components icons cleanup (#14294) * update icon shapes to match new design system icons * add AngleUpIcon to storybook * rename Family icon to Users to match design system naming conventions * add Collection icon * move illustrations to their own path/category to match design system convention * remove hardcoded PartyHorn illustration size * fix swapped story names * rename PartyHorn illustration to Celebrate to match design system convention * update Warning illustration to use new design system shape --- .../cipher/cipher-indicator-icons.ts | 4 +- .../content/components/icons/angle-down.ts | 4 +- .../content/components/icons/angle-up.ts | 9 +--- .../content/components/icons/business.ts | 19 ++------ .../content/components/icons/close.ts | 4 +- .../content/components/icons/collection.ts | 23 ++++++++++ .../components/icons/exclamation-triangle.ts | 14 +++++- .../content/components/icons/family.ts | 18 -------- .../content/components/icons/folder.ts | 5 ++- .../content/components/icons/globe.ts | 5 +-- .../content/components/icons/index.ts | 6 +-- .../content/components/icons/pencil-square.ts | 8 +++- .../content/components/icons/shield.ts | 4 +- .../autofill/content/components/icons/user.ts | 4 +- .../content/components/icons/users.ts | 18 ++++++++ .../content/components/icons/warning.ts | 23 ---------- .../celebrate.ts} | 12 ++--- .../content/components/illustrations/index.ts | 3 ++ .../{icons => illustrations}/keyhole.ts | 0 .../components/illustrations/warning.ts | 22 ++++++++++ .../lit-stories/.lit-docs/icons.mdx | 10 ++--- ... => cipher-indicator-icons.lit-stories.ts} | 2 +- .../lit-stories/icons/icons.lit-stories.ts | 10 ++--- .../illustrations.lit-stories.ts | 44 +++++++++++++++++++ .../components/notification/button-row.ts | 4 +- .../notification/confirmation/body.ts | 4 +- 26 files changed, 168 insertions(+), 111 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/icons/collection.ts delete mode 100644 apps/browser/src/autofill/content/components/icons/family.ts create mode 100644 apps/browser/src/autofill/content/components/icons/users.ts delete mode 100644 apps/browser/src/autofill/content/components/icons/warning.ts rename apps/browser/src/autofill/content/components/{icons/party-horn.ts => illustrations/celebrate.ts} (98%) create mode 100644 apps/browser/src/autofill/content/components/illustrations/index.ts rename apps/browser/src/autofill/content/components/{icons => illustrations}/keyhole.ts (100%) create mode 100644 apps/browser/src/autofill/content/components/illustrations/warning.ts rename apps/browser/src/autofill/content/components/lit-stories/ciphers/{cipher-indicator-icon.lit-stories.ts => cipher-indicator-icons.lit-stories.ts} (94%) create mode 100644 apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index 39d4dd28f24..9096149f510 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -4,7 +4,7 @@ import { html } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; -import { Business, Family } from "../../../content/components/icons"; +import { Business, Users } from "../../../content/components/icons"; // @TODO connect data source to icon checks // @TODO support other indicator types (attachments, etc) @@ -19,7 +19,7 @@ export function CipherInfoIndicatorIcons({ }) { const indicatorIcons = [ ...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []), - ...(showFamilyIcon ? [Family({ color: themes[theme].text.muted, theme })] : []), + ...(showFamilyIcon ? [Users({ color: themes[theme].text.muted, theme })] : []), ]; return indicatorIcons.length diff --git a/apps/browser/src/autofill/content/components/icons/angle-down.ts b/apps/browser/src/autofill/content/components/icons/angle-down.ts index db5275aafa9..27cd5ab81c5 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-down.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-down.ts @@ -8,10 +8,10 @@ export function AngleDown({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/angle-up.ts b/apps/browser/src/autofill/content/components/icons/angle-up.ts index 7344123d5ad..f8bda632285 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-up.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-up.ts @@ -8,15 +8,10 @@ export function AngleUp({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/business.ts b/apps/browser/src/autofill/content/components/icons/business.ts index ef8e082c21f..79e64a0a1f9 100644 --- a/apps/browser/src/autofill/content/components/icons/business.ts +++ b/apps/browser/src/autofill/content/components/icons/business.ts @@ -8,30 +8,17 @@ export function Business({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + - - `; } diff --git a/apps/browser/src/autofill/content/components/icons/close.ts b/apps/browser/src/autofill/content/components/icons/close.ts index c9d9286ca3f..27610bc7773 100644 --- a/apps/browser/src/autofill/content/components/icons/close.ts +++ b/apps/browser/src/autofill/content/components/icons/close.ts @@ -8,10 +8,10 @@ export function Close({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/collection.ts b/apps/browser/src/autofill/content/components/icons/collection.ts new file mode 100644 index 00000000000..fb2c58647c5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/collection.ts @@ -0,0 +1,23 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Collection({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts index d87d5621e30..c4f587b2d7b 100644 --- a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts +++ b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts @@ -8,10 +8,20 @@ export function ExclamationTriangle({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + + `; diff --git a/apps/browser/src/autofill/content/components/icons/family.ts b/apps/browser/src/autofill/content/components/icons/family.ts deleted file mode 100644 index 9870c5d37c0..00000000000 --- a/apps/browser/src/autofill/content/components/icons/family.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { IconProps } from "../common-types"; -import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; - -export function Family({ color, disabled, theme }: IconProps) { - const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; - - return html` - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/icons/folder.ts b/apps/browser/src/autofill/content/components/icons/folder.ts index 84577aef820..1b93d2d32ea 100644 --- a/apps/browser/src/autofill/content/components/icons/folder.ts +++ b/apps/browser/src/autofill/content/components/icons/folder.ts @@ -8,10 +8,11 @@ export function Folder({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/globe.ts b/apps/browser/src/autofill/content/components/icons/globe.ts index fc0a975284d..936fd8d2802 100644 --- a/apps/browser/src/autofill/content/components/icons/globe.ts +++ b/apps/browser/src/autofill/content/components/icons/globe.ts @@ -8,11 +8,10 @@ export function Globe({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index 4b6cb7abdd8..de39b70ab24 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -3,14 +3,12 @@ export { AngleUp } from "./angle-up"; export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; +export { Collection } from "./collection"; export { ExclamationTriangle } from "./exclamation-triangle"; export { ExternalLink } from "./external-link"; -export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; -export { Keyhole } from "./keyhole"; -export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; export { User } from "./user"; -export { Warning } from "./warning"; +export { Users } from "./users"; diff --git a/apps/browser/src/autofill/content/components/icons/pencil-square.ts b/apps/browser/src/autofill/content/components/icons/pencil-square.ts index f41ab927809..11366f2631a 100644 --- a/apps/browser/src/autofill/content/components/icons/pencil-square.ts +++ b/apps/browser/src/autofill/content/components/icons/pencil-square.ts @@ -8,10 +8,14 @@ export function PencilSquare({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + `; diff --git a/apps/browser/src/autofill/content/components/icons/shield.ts b/apps/browser/src/autofill/content/components/icons/shield.ts index 5a2d7d39d58..a027dd3e113 100644 --- a/apps/browser/src/autofill/content/components/icons/shield.ts +++ b/apps/browser/src/autofill/content/components/icons/shield.ts @@ -8,10 +8,10 @@ export function Shield({ color, theme }: IconProps) { const shapeColor = color || themes[theme].brandLogo; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/user.ts b/apps/browser/src/autofill/content/components/icons/user.ts index 32ccd3a2031..b59204a0ad8 100644 --- a/apps/browser/src/autofill/content/components/icons/user.ts +++ b/apps/browser/src/autofill/content/components/icons/user.ts @@ -8,10 +8,10 @@ export function User({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/users.ts b/apps/browser/src/autofill/content/components/icons/users.ts new file mode 100644 index 00000000000..eb7840104f0 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/users.ts @@ -0,0 +1,18 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Users({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/warning.ts b/apps/browser/src/autofill/content/components/icons/warning.ts deleted file mode 100644 index 9ae9aeca352..00000000000 --- a/apps/browser/src/autofill/content/components/icons/warning.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { html } from "lit"; - -// This icon has static multi-colors for each theme -export function Warning() { - return html` - - - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/illustrations/celebrate.ts similarity index 98% rename from apps/browser/src/autofill/content/components/icons/party-horn.ts rename to apps/browser/src/autofill/content/components/illustrations/celebrate.ts index 439d60a79de..30b3743004f 100644 --- a/apps/browser/src/autofill/content/components/icons/party-horn.ts +++ b/apps/browser/src/autofill/content/components/illustrations/celebrate.ts @@ -5,16 +5,10 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { IconProps } from "../common-types"; // This icon has static multi-colors for each theme -export function PartyHorn({ theme }: IconProps) { +export function Celebrate({ theme }: IconProps) { if (theme === ThemeTypes.Dark) { return html` - + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx index f85cf3ae90f..571ed10285a 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx @@ -2,7 +2,7 @@ import { Meta, Controls } from "@storybook/addon-docs"; import * as stories from "./icons.lit-stories"; - + ## Icon Stories @@ -14,12 +14,12 @@ like size, color, and theme. Each story is an example of how a specific icon can | | | | ------------------------- | ------------------ | -| `AngleDownIcon` | `FolderIcon` | -| `BusinessIcon` | `GlobeIcon` | -| `BrandIcon` | `PartyHornIcon` | +| `AngleDownIcon` | `AngleUpIcon` | +| `BusinessIcon` | `FolderIcon` | +| `BrandIcon` | `GlobeIcon` | | `CloseIcon` | `PencilSquareIcon` | | `ExclamationTriangleIcon` | `ShieldIcon` | -| `FamilyIcon` | `UserIcon` | +| `UsersIcon` | `UserIcon` | ## Props diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts similarity index 94% rename from apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts index 89c3ecbcb1c..08530452730 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Ciphers/Cipher Indicator Icon", + title: "Components/Ciphers/Cipher Indicator Icons", argTypes: { showBusinessIcon: { control: "boolean" }, showFamilyIcon: { control: "boolean" }, diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index 8bd87ef6674..fc5db1c7c2c 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -14,7 +14,7 @@ type Args = { }; export default { - title: "Components/Icons/Icons", + title: "Components/Icons", argTypes: { iconLink: { control: "text" }, color: { control: "color" }, @@ -53,16 +53,16 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj => { }; export const AngleDownIcon = createIconStory("AngleDown"); -export const BusinessIcon = createIconStory("Business"); +export const AngleUpIcon = createIconStory("AngleUp"); export const BrandIcon = createIconStory("BrandIconContainer"); +export const BusinessIcon = createIconStory("Business"); export const CloseIcon = createIconStory("Close"); +export const CollectionIcon = createIconStory("Collection"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); export const ExternalLinkIcon = createIconStory("ExternalLink"); -export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); -export const KeyholeIcon = createIconStory("Keyhole"); -export const PartyHornIcon = createIconStory("PartyHorn"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); export const UserIcon = createIconStory("User"); +export const UsersIcon = createIconStory("Users"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts new file mode 100644 index 00000000000..86d55f2f795 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts @@ -0,0 +1,44 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import * as Illustrations from "../../illustrations"; + +type Args = { + theme: Theme; + size: number; +}; + +export default { + title: "Components/Illustrations", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + size: { control: "number", min: 10, max: 100, step: 1 }, + }, + args: { + theme: ThemeTypes.Light, + size: 50, + }, +} as Meta; + +const Template = ( + args: Args, + IllustrationComponent: (props: Args) => ReturnType, +) => html` +
+ ${IllustrationComponent({ ...args })} +
+`; + +const createIllustrationStory = (illustrationName: keyof typeof Illustrations): StoryObj => { + return { + render: (args) => Template(args, Illustrations[illustrationName]), + } as StoryObj; +}; + +export const KeyholeIllustration = createIllustrationStory("Keyhole"); +export const CelebrateIllustration = createIllustrationStory("Celebrate"); +export const WarningIllustration = createIllustrationStory("Warning"); diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 8661f5957e1..6fa32f11aa2 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -4,14 +4,14 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { Theme } from "@bitwarden/common/platform/enums"; import { Option, OrgView, FolderView } from "../common-types"; -import { Business, Family, Folder, User } from "../icons"; +import { Business, Users, Folder, User } from "../icons"; import { ButtonRow } from "../rows/button-row"; function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] { switch (productTierType) { case ProductTierType.Free: case ProductTierType.Families: - return Family; + return Users; case ProductTierType.Teams: case ProductTierType.Enterprise: case ProductTierType.TeamsStarter: diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 55d257b36f4..d2ac7f36277 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -4,7 +4,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../constants/styles"; -import { PartyHorn, Keyhole, Warning } from "../../icons"; +import { Celebrate, Keyhole, Warning } from "../../illustrations"; import { NotificationConfirmationMessage } from "./message"; @@ -33,7 +33,7 @@ export function NotificationConfirmationBody({ theme, handleOpenVault, }: NotificationConfirmationBodyProps) { - const IconComponent = tasksAreComplete ? Keyhole : !error ? PartyHorn : Warning; + const IconComponent = tasksAreComplete ? Keyhole : !error ? Celebrate : Warning; const showConfirmationMessage = confirmationMessage || buttonText || messageDetails; From a61d8780816c0c57710f76ff331e1635b15e247a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 15 Apr 2025 17:19:58 -0400 Subject: [PATCH 02/45] PM-20106 Pass indicator data to notification bar cipher items (#14246) * PM-20106 initial approach whihc preserves exisiting indicator file style * refactored approach to be able to pass any icon when or if needed in the future * address feedback --- .../background/notification.background.ts | 31 ++++++++++++++-- .../cipher/cipher-indicator-icons.ts | 35 +++++++++++-------- .../content/components/cipher/cipher-info.ts | 16 ++++++--- .../content/components/cipher/types.ts | 9 +++++ 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 1f0cc469e2c..6589252d94b 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -16,6 +16,7 @@ import { } 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 { ProductTierType } from "@bitwarden/common/billing/enums/product-tier-type.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -41,7 +42,11 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; -import { NotificationCipherData } from "../content/components/cipher/types"; +import { + OrganizationCategory, + OrganizationCategories, + NotificationCipherData, +} from "../content/components/cipher/types"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; @@ -174,8 +179,29 @@ export default class NotificationBackground { activeUserId, ); + const organizations = await firstValueFrom( + this.organizationService.organizations$(activeUserId), + ); + return decryptedCiphers.map((view) => { - const { id, name, reprompt, favorite, login } = view; + const { id, name, reprompt, favorite, login, organizationId } = view; + + const organizationType = organizationId + ? organizations.find((org) => org.id === organizationId)?.productTierType + : null; + + const organizationCategories: OrganizationCategory[] = []; + + if ( + [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( + organizationType, + ) + ) { + organizationCategories.push(OrganizationCategories.business); + } + if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { + organizationCategories.push(OrganizationCategories.family); + } return { id, @@ -183,6 +209,7 @@ export default class NotificationBackground { type: CipherType.Login, reprompt, favorite, + ...(organizationCategories.length ? { organizationCategories } : {}), icon: buildCipherIcon(iconsServerUrl, view, showFavicons), login: login && { username: login.username, diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index 9096149f510..e4fe012a678 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -1,30 +1,35 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; import { Business, Users } from "../../../content/components/icons"; -// @TODO connect data source to icon checks -// @TODO support other indicator types (attachments, etc) +import { OrganizationCategories, OrganizationCategory } from "./types"; + +const cipherIndicatorIconsMap: Record< + OrganizationCategory, + (args: { color: string; theme: Theme }) => TemplateResult +> = { + [OrganizationCategories.business]: Business, + [OrganizationCategories.family]: Users, +}; + export function CipherInfoIndicatorIcons({ - showBusinessIcon, - showFamilyIcon, + organizationCategories = [], theme, }: { - showBusinessIcon?: boolean; - showFamilyIcon?: boolean; + organizationCategories?: OrganizationCategory[]; theme: Theme; }) { - const indicatorIcons = [ - ...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []), - ...(showFamilyIcon ? [Users({ color: themes[theme].text.muted, theme })] : []), - ]; - - return indicatorIcons.length - ? html` ${indicatorIcons} ` - : null; // @TODO null case should be handled by parent + return html` + + ${organizationCategories.map((name) => + cipherIndicatorIconsMap[name]?.({ color: themes[theme].text.muted, theme }), + )} + + `; } const cipherInfoIndicatorIconsStyles = css` diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts index 6ff32353938..e3d237b9bc6 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -8,14 +8,22 @@ import { themes, typography } from "../../../content/components/constants/styles import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons"; import { NotificationCipherData } from "./types"; -// @TODO support other cipher types (card, identity, notes, etc) export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) { - const { name, login } = cipher; + const { name, login, organizationCategories } = cipher; + const hasIndicatorIcons = organizationCategories?.length; return html`
- ${[name, CipherInfoIndicatorIcons({ theme })]} + ${[ + name, + hasIndicatorIcons + ? CipherInfoIndicatorIcons({ + theme, + organizationCategories, + }) + : nothing, + ]} ${login?.username diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts index ff29f9b559f..590311682bf 100644 --- a/apps/browser/src/autofill/content/components/cipher/types.ts +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -14,6 +14,14 @@ export const CipherRepromptTypes = { type CipherRepromptType = (typeof CipherRepromptTypes)[keyof typeof CipherRepromptTypes]; +export type OrganizationCategory = + (typeof OrganizationCategories)[keyof typeof OrganizationCategories]; + +export const OrganizationCategories = { + business: "business", + family: "family", +} as const; + export type WebsiteIconData = { imageEnabled: boolean; image: string; @@ -50,4 +58,5 @@ export type NotificationCipherData = BaseCipherData & login?: { username: string; }; + organizationCategories?: OrganizationCategory[]; }; From cb869484239c01075e6145681c240c8c43f21a4c Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Tue, 15 Apr 2025 20:00:08 -0400 Subject: [PATCH 03/45] [PM-15436] Standalone password entry should trigger save to bitwarden prompt. (#14110) * Modify behavior so standalone password entry (with or without generator) should trigger save to bitwarden prompt. * Rename intent to action, extend button/action styles. * Ensure font weight is returned to normal. * Make save login message a button to handle accessibility, adds helper function. * Fix failing snapshot by reintigrating erroneously removed line. * Update snapshot to match new saveLoginButton. * Add add'l open in new window message to aria label. * Update snapshot with open in new window message. --- apps/browser/src/_locales/en/messages.json | 4 +- .../autofill/background/overlay.background.ts | 6 +-- .../autofill-inline-menu-list.spec.ts.snap | 39 ++------------- .../list/autofill-inline-menu-list.spec.ts | 10 ++-- .../pages/list/autofill-inline-menu-list.ts | 50 +++++++++++++++---- .../overlay/inline-menu/pages/list/list.scss | 8 +++ 6 files changed, 61 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f3b85496b75..87b94650b51 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index a2088f50a11..4e2e773a0c7 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1852,7 +1852,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { /** * Verifies whether the save login inline menu view should be shown. This requires that - * the login data on the page contains a username and either a current or new password. + * the login data on the page contains either a current or new password. * * @param tab - The tab to check for login data */ @@ -1869,7 +1869,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return ( (this.shouldShowInlineMenuAccountCreation() || this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration)) && - !!(loginData.username && (loginData.password || loginData.newPassword)) + !!(loginData.password || loginData.newPassword) ); } @@ -2157,7 +2157,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { "passwordRegenerated", "passwords", "regeneratePassword", - "saveLoginToBitwarden", + "saveToBitwarden", "toggleBitwardenVaultOverlay", "totpCodeAria", "totpSecondsSpanAria", diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index acd06fb8c65..b6e41c448d6 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -4,47 +4,14 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList creates the build sav
- `; diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index b1eebd2bc39..ed28375e4fe 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -1089,12 +1089,12 @@ describe("AutofillInlineMenuList", () => { }); describe("displaying the save login view", () => { - let buildSaveLoginInlineMenuListSpy: jest.SpyInstance; + let buildSaveLoginInlineMenuSpy: jest.SpyInstance; beforeEach(() => { - buildSaveLoginInlineMenuListSpy = jest.spyOn( + buildSaveLoginInlineMenuSpy = jest.spyOn( autofillInlineMenuList as any, - "buildSaveLoginInlineMenuList", + "buildSaveLoginInlineMenu", ); }); @@ -1108,7 +1108,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage({ command: "showSaveLoginInlineMenuList" }); - expect(buildSaveLoginInlineMenuListSpy).not.toHaveBeenCalled(); + expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled(); }); it("builds the save login item view", async () => { @@ -1117,7 +1117,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage({ command: "showSaveLoginInlineMenuList" }); - expect(buildSaveLoginInlineMenuListSpy).toHaveBeenCalled(); + expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled(); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index acb01594cc6..e0db93b6b4a 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -3,6 +3,8 @@ import "@webcomponents/custom-elements"; import "lit/polyfill-support.js"; +import { FocusableElement } from "tabbable"; + import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -117,7 +119,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { } if (showSaveLoginMenu) { - this.buildSaveLoginInlineMenuList(); + this.buildSaveLoginInlineMenu(); return; } @@ -165,24 +167,52 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { /** * Builds the inline menu list as a prompt that asks the user if they'd like to save the login data. */ - private buildSaveLoginInlineMenuList() { - const saveLoginMessage = globalThis.document.createElement("div"); - saveLoginMessage.classList.add("save-login", "inline-menu-list-message"); - saveLoginMessage.textContent = this.getTranslation("saveLoginToBitwarden"); + private buildSaveLoginInlineMenu() { + const saveLoginButton = globalThis.document.createElement("button"); + saveLoginButton.classList.add( + "save-login", + "inline-menu-list-button", + "inline-menu-list-action", + ); + + saveLoginButton.tabIndex = -1; + saveLoginButton.setAttribute( + "aria-label", + `${this.getTranslation("saveToBitwarden")}, ${this.getTranslation("opensInANewWindow")}`, + ); + saveLoginButton.textContent = this.getTranslation("saveToBitwarden"); + + saveLoginButton.addEventListener(EVENTS.CLICK, this.handleNewLoginVaultItemAction); + saveLoginButton.addEventListener(EVENTS.KEYUP, this.handleSaveLoginInlineMenuKeyUp); + + const inlineMenuListButtonContainer = this.buildButtonContainer(saveLoginButton); - const newItemButton = this.buildNewItemButton(true); this.showInlineMenuAccountCreation = true; - this.inlineMenuListContainer.append(saveLoginMessage, newItemButton); + this.inlineMenuListContainer.append(inlineMenuListButtonContainer); } + private handleSaveLoginInlineMenuKeyUp = (event: KeyboardEvent) => { + const listenedForKeys = new Set(["ArrowDown"]); + if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) { + return; + } + + event.preventDefault(); + + if (event.code === "ArrowDown") { + (event.target as FocusableElement).focus(); + return; + } + }; + /** * Handles the show save login inline menu list message that is triggered from the background script. */ private handleShowSaveLoginInlineMenuList() { if (this.authStatus === AuthenticationStatus.Unlocked) { this.resetInlineMenuContainer(); - this.buildSaveLoginInlineMenuList(); + this.buildSaveLoginInlineMenu(); } } @@ -521,7 +551,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { this.newItemButtonElement.textContent = this.getNewItemButtonText(showLogin); this.newItemButtonElement.setAttribute("aria-label", this.getNewItemAriaLabel(showLogin)); this.newItemButtonElement.prepend(buildSvgDomElement(plusIcon)); - this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick); + this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handleNewLoginVaultItemAction); return this.buildButtonContainer(this.newItemButtonElement); } @@ -581,7 +611,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles the click event for the new item button. * Sends a message to the parent window to add a new vault item. */ - private handeNewItemButtonClick = () => { + private handleNewLoginVaultItemAction = () => { let addNewCipherType = this.inlineMenuFillType; if (this.showInlineMenuAccountCreation) { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss index d0875cfe427..93f5f647ffe 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss @@ -45,6 +45,14 @@ body * { &.no-items, &.save-login { font-size: 1.6rem; + &:has(:focus-visible) { + outline-width: 0.2rem; + outline-style: solid; + + @include themify($themes) { + outline-color: themed("focusOutlineColor"); + } + } } } From 9da15601be3b8a7e2883db55b7ab0a0a8150ccb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Wed, 16 Apr 2025 15:06:41 +0200 Subject: [PATCH 04/45] Add workflow to trigger self-host unified build in publish web (#14268) --- .github/workflows/publish-web.yml | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 09f5ddc6318..69b29086d36 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -141,3 +141,36 @@ jobs: - name: Log out of Docker run: docker logout + + self-host-unified-build: + name: Trigger self-host unified build + runs-on: ubuntu-22.04 + needs: + - setup + steps: + - name: Log in to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Trigger self-host build + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'self-host', + workflow_id: 'build-unified.yml', + ref: 'main', + inputs: { + use_latest_core_version: true + } + }); From 9cffc3b4f4c61aa57dcc62b2298f4840e3906a18 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 16 Apr 2025 08:16:40 -0500 Subject: [PATCH 05/45] [PM-20118] Capitalize risk insights (#14291) --- apps/web/src/locales/en/messages.json | 4 ++-- .../access-intelligence/risk-insights-loading.component.html | 2 +- .../tools/access-intelligence/risk-insights.component.html | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0193fc4862b..85a7b8cb927 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4073,8 +4073,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html index 4e77838229e..0c5b74eead2 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html @@ -4,5 +4,5 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > -

{{ "generatingRiskInsights" | i18n }}

+

{{ "generatingYourRiskInsights" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 397e2a630de..2d5693dad54 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -8,6 +8,7 @@ {{ "reviewAtRiskPasswords" | i18n }}
Date: Wed, 16 Apr 2025 11:04:31 -0400 Subject: [PATCH 06/45] fix restore button (#14244) --- apps/desktop/src/vault/app/vault/view.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index ede6eb7ed82..8477a588fef 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -656,7 +656,11 @@ class="primary" (click)="restore()" appA11yTitle="{{ 'restore' | i18n }}" - *ngIf="(limitItemDeletion$ | async) ? (canRestoreCipher$ | async) : cipher.isDeleted" + *ngIf=" + (limitItemDeletion$ | async) + ? (canRestoreCipher$ | async) && cipher.isDeleted + : cipher.isDeleted + " > From b413272bd5adc63b2c77bdeceddf23d7f0603208 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 16 Apr 2025 11:08:51 -0400 Subject: [PATCH 07/45] [PM-20325] - Misc design fixes/tweaks (#14309) * fix icon sizing in option selection * fix close button vertical centering * fix cipher item update text * fix missing header background color * fix brand logo positioning in notification header --- .../autofill/content/components/buttons/close-button.ts | 1 + .../components/buttons/option-selection-button.ts | 5 +++-- .../autofill/content/components/cipher/cipher-action.ts | 4 ++-- .../content/components/icons/brand-icon-container.ts | 9 +++++++-- .../autofill/content/components/notification/header.ts | 2 +- .../content/components/option-selection/option-item.ts | 7 ++++--- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/autofill/content/components/buttons/close-button.ts b/apps/browser/src/autofill/content/components/buttons/close-button.ts index c32d0c130e3..05a12d4f453 100644 --- a/apps/browser/src/autofill/content/components/buttons/close-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/close-button.ts @@ -35,5 +35,6 @@ const closeButtonStyles = (theme: Theme) => css` > svg { width: 20px; height: 20px; + vertical-align: middle; } `; diff --git a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts index cf9a561ee39..e3c7e0d54e6 100644 --- a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts @@ -44,7 +44,7 @@ export function OptionSelectionButton({ `; } -const iconSize = "15px"; +const iconSize = "16px"; const selectionButtonStyles = ({ disabled, @@ -94,7 +94,8 @@ const selectionButtonStyles = ({ > svg { max-width: ${iconSize}; - height: fit-content; + max-height: ${iconSize}; + height: auto; } `; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index 2d386d34d6a..aaa4b11d8a2 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -19,13 +19,13 @@ export function CipherAction({ ? BadgeButton({ buttonAction: handleAction, // @TODO localize - buttonText: "Update item", + buttonText: "Update", theme, }) : EditButton({ buttonAction: handleAction, // @TODO localize - buttonText: "Edit item", + buttonText: "Edit", theme, }); } diff --git a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts index 8df68d79b6e..1b08f261eb6 100644 --- a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts +++ b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts @@ -12,8 +12,13 @@ export function BrandIconContainer({ iconLink, theme }: { iconLink?: URL; theme: } const brandIconContainerStyles = css` + display: flex; + justify-content: center; + width: 24px; + height: 24px; + > svg { - width: 20px; - height: fit-content; + width: auto; + height: 100%; } `; diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts index 50c2c629942..d6cedf6a85a 100644 --- a/apps/browser/src/autofill/content/components/notification/header.ts +++ b/apps/browser/src/autofill/content/components/notification/header.ts @@ -49,7 +49,7 @@ const notificationHeaderStyles = ({ display: flex; align-items: center; justify-content: flex-start; - background-color: ${themes[theme].background}; + background-color: ${themes[theme].background.DEFAULT}; padding: 12px 16px 8px 16px; white-space: nowrap; diff --git a/apps/browser/src/autofill/content/components/option-selection/option-item.ts b/apps/browser/src/autofill/content/components/option-selection/option-item.ts index 619d77e63d3..e8a293e2c3f 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-item.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-item.ts @@ -62,14 +62,15 @@ const optionItemStyles = css` `; const optionItemIconContainerStyles = css` + display: flex; flex-grow: 1; flex-shrink: 1; - width: ${optionItemIconWidth}px; - height: ${optionItemIconWidth}px; + max-width: ${optionItemIconWidth}px; + max-height: ${optionItemIconWidth}px; > svg { width: 100%; - height: fit-content; + height: auto; } `; From f293c15f4d8ae9bdae07e1f8a26232da2c456243 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 16 Apr 2025 08:24:30 -0700 Subject: [PATCH 08/45] [PM-19538] Add shareReplay to internal orgKeys subscription (#14034) --- .../collections/services/default-collection.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts index da50a25886e..1ae58d3eef3 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -8,10 +8,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ActiveUserState, - StateProvider, COLLECTION_DATA, DeriveDefinition, DerivedState, + StateProvider, UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; @@ -84,6 +84,7 @@ export class DefaultCollectionService implements CollectionService { switchMap(([userId, collectionData]) => combineLatest([of(collectionData), this.keyService.orgKeys$(userId)]), ), + shareReplay({ refCount: false, bufferSize: 1 }), ); this.decryptedCollectionDataState = this.stateProvider.getDerived( From db16c98a1d93114b613398f0758c15bdbc7faf20 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:58:54 -0400 Subject: [PATCH 09/45] [PM-17773] Added "Sponsored Families" dropdown nav item in the admin console (#14029) * Added nav item for f4e in org admin console * shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table * Resolved issue with members nav item also being selected when f4e is selected * Separated out billing's logic from the org layout component * Removed unused observable * Moved logic to existing f4e policy service and added unit tests * Resolved script typescript error * Resolved goofy switchMap --------- Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> --- .../organization-layout.component.html | 26 ++- .../layouts/organization-layout.component.ts | 5 + .../members/members-routing.module.ts | 9 + .../free-families-policy.service.spec.ts | 193 ++++++++++++++++++ .../services/free-families-policy.service.ts | 46 +++++ .../models/data/organization.data.spec.ts | 1 + .../models/data/organization.data.ts | 2 + .../models/domain/organization.ts | 2 + .../response/profile-organization.response.ts | 2 + libs/common/src/enums/feature-flag.enum.ts | 2 + 10 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/app/billing/services/free-families-policy.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 47846c77571..e50c55e83d2 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -19,12 +19,26 @@ *ngIf="canShowVaultTab(organization)" > - + + + + + + + + + + + + + ; protected isBreadcrumbEventLogsEnabled$: Observable; + protected showSponsoredFamiliesDropdown$: Observable; constructor( private route: ActivatedRoute, @@ -76,6 +78,7 @@ export class OrganizationLayoutComponent implements OnInit { private providerService: ProviderService, protected bannerService: AccountDeprovisioningBannerService, private accountService: AccountService, + private freeFamiliesPolicyService: FreeFamiliesPolicyService, ) {} async ngOnInit() { @@ -92,6 +95,8 @@ export class OrganizationLayoutComponent implements OnInit { ), filter((org) => org != null), ); + this.showSponsoredFamiliesDropdown$ = + this.freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(this.organization$); this.showAccountDeprovisioningBanner$ = combineLatest([ this.bannerService.showBanner$, diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index 5220ea1ef39..9666630fc08 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; import { MembersComponent } from "./members.component"; @@ -16,6 +17,14 @@ const routes: Routes = [ titleId: "members", }, }, + { + path: "sponsored-families", + component: SponsoredFamiliesComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab)], + data: { + titleId: "sponsoredFamilies", + }, + }, ]; @NgModule({ diff --git a/apps/web/src/app/billing/services/free-families-policy.service.spec.ts b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts new file mode 100644 index 00000000000..10ccc448986 --- /dev/null +++ b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts @@ -0,0 +1,193 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { FreeFamiliesPolicyService } from "./free-families-policy.service"; + +describe("FreeFamiliesPolicyService", () => { + let service: FreeFamiliesPolicyService; + let organizationService: MockProxy; + let policyService: MockProxy; + let configService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; + + beforeEach(() => { + organizationService = mock(); + policyService = mock(); + configService = mock(); + accountService = mockAccountServiceWith(userId); + + service = new FreeFamiliesPolicyService( + policyService, + organizationService, + accountService, + configService, + ); + }); + + describe("showSponsoredFamiliesDropdown$", () => { + it("should return true when all conditions are met", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization that meets all criteria + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: true, + isOwner: false, + canManageUsers: false, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(true); + }); + + it("should return false when organization is not Enterprise", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization that is not Enterprise tier + const organization = { + id: "org-id", + productTierType: ProductTierType.Teams, + useAdminSponsoredFamilies: true, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return false when feature flag is disabled", async () => { + // Configure mocks to disable feature flag + configService.getFeatureFlag$.mockReturnValue(of(false)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization that meets other criteria + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return false when families feature is disabled by policy", async () => { + // Configure mocks with a policy that disables the feature + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue( + of([{ organizationId: "org-id", enabled: true } as Policy]), + ); + + // Create a test organization + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return false when useAdminSponsoredFamilies is false", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization with useAdminSponsoredFamilies set to false + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: false, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return true when user is an owner but not admin", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization where user is owner but not admin + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: false, + isOwner: true, + canManageUsers: false, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(true); + }); + + it("should return true when user can manage users but is not admin or owner", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization where user can manage users but is not admin or owner + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: false, + isOwner: false, + canManageUsers: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(true); + }); + + it("should return false when user has no admin permissions", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization where user has no admin permissions + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: false, + isOwner: false, + canManageUsers: false, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index 81cb970cdbe..7a8e3804b2c 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -7,6 +7,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; interface EnterpriseOrgStatus { isFreeFamilyPolicyEnabled: boolean; @@ -26,6 +29,7 @@ export class FreeFamiliesPolicyService { private policyService: PolicyService, private organizationService: OrganizationService, private accountService: AccountService, + private configService: ConfigService, ) {} organizations$ = this.accountService.activeAccount$.pipe( @@ -42,6 +46,48 @@ export class FreeFamiliesPolicyService { return this.getFreeFamiliesVisibility$(); } + /** + * Determines whether to show the sponsored families dropdown in the organization layout + * @param organization The organization to check + * @returns Observable indicating whether to show the dropdown + */ + showSponsoredFamiliesDropdown$(organization: Observable): Observable { + const enterpriseOrganization$ = organization.pipe( + map((org) => org.productTierType === ProductTierType.Enterprise), + ); + + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => { + const policies$ = this.policyService.policiesByType$( + PolicyType.FreeFamiliesSponsorshipPolicy, + userId, + ); + + return combineLatest([ + enterpriseOrganization$, + this.configService.getFeatureFlag$(FeatureFlag.PM17772_AdminInitiatedSponsorships), + organization, + policies$, + ]).pipe( + map(([isEnterprise, featureFlagEnabled, org, policies]) => { + const familiesFeatureDisabled = policies.some( + (policy) => policy.organizationId === org.id && policy.enabled, + ); + + return ( + isEnterprise && + featureFlagEnabled && + !familiesFeatureDisabled && + org.useAdminSponsoredFamilies && + (org.isAdmin || org.isOwner || org.canManageUsers) + ); + }), + ); + }), + ); + } + private getFreeFamiliesVisibility$(): Observable { return combineLatest([ this.checkEnterpriseOrganizationsAndFetchPolicy(), diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index 5f487e1f898..fae24133502 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -58,6 +58,7 @@ describe("ORGANIZATIONS state", () => { familySponsorshipLastSyncDate: new Date(), userIsManagedByOrganization: false, useRiskInsights: false, + useAdminSponsoredFamilies: false, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index b81d06e6367..799d062aefa 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -60,6 +60,7 @@ export class OrganizationData { allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; + useAdminSponsoredFamilies: boolean; constructor( response?: ProfileOrganizationResponse, @@ -122,6 +123,7 @@ export class OrganizationData { this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = response.userIsManagedByOrganization; this.useRiskInsights = response.useRiskInsights; + this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies; this.isMember = options.isMember; this.isProviderUser = options.isProviderUser; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index c5c5b53cce7..2e51c54b0ad 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -90,6 +90,7 @@ export class Organization { */ userIsManagedByOrganization: boolean; useRiskInsights: boolean; + useAdminSponsoredFamilies: boolean; constructor(obj?: OrganizationData) { if (obj == null) { @@ -148,6 +149,7 @@ export class Organization { this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = obj.userIsManagedByOrganization; this.useRiskInsights = obj.useRiskInsights; + this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index 5e37cfc4c5c..da97a1034b1 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -55,6 +55,7 @@ export class ProfileOrganizationResponse extends BaseResponse { allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; + useAdminSponsoredFamilies: boolean; constructor(response: any) { super(response); @@ -121,5 +122,6 @@ export class ProfileOrganizationResponse extends BaseResponse { ); this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization"); this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); + this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies"); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fa776285ead..9ee1ef919f5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -34,6 +34,7 @@ export enum FeatureFlag { PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", + PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", /* Key Management */ PrivateKeyRegeneration = "pm-12241-private-key-regeneration", @@ -117,6 +118,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, + [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, From 6bd3fceaa1eac26fb8116fde0f308bbc40e77922 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:27:48 +0100 Subject: [PATCH 10/45] fix: align upgrade badge with header text in Event Logs (#14213) --- .../organizations/manage/events.component.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 80d22467123..2079d592a28 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,6 +1,12 @@ @let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async); - + {{ "upgrade" | i18n }} From 1efdcacd16d5d1fd9e0c853976c73491d40e839d Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 16 Apr 2025 13:15:43 -0400 Subject: [PATCH 11/45] [PM-16641] Remove "inline-menu-positioning-improvements" feature flag (#14225) * remove inline-menu-positioning-improvements flag * remove unused LegacyOverlayBackground * remove unused deprecated files * appease ts error TS2564 * remove deleted resources from the manifest files --- .../autofill/background/tabs.background.ts | 14 +- .../overlay.background.deprecated.ts | 124 -- .../overlay.background.deprecated.spec.ts | 1464 -------------- .../overlay.background.deprecated.ts | 811 -------- .../abstractions/autofill-init.deprecated.ts | 41 - .../content/autofill-init.deprecated.spec.ts | 604 ------ .../content/autofill-init.deprecated.ts | 315 --- .../bootstrap-legacy-autofill-overlay.ts | 14 - .../autofill-overlay-button.deprecated.ts | 29 - ...ofill-overlay-iframe.service.deprecated.ts | 33 - .../autofill-overlay-list.deprecated.ts | 31 - ...utofill-overlay-page-element.deprecated.ts | 13 - ...lay-iframe.service.deprecated.spec.ts.snap | 23 - ...l-overlay-button-iframe.deprecated.spec.ts | 26 - ...tofill-overlay-button-iframe.deprecated.ts | 21 - ...-overlay-iframe-element.deprecated.spec.ts | 46 - ...ofill-overlay-iframe-element.deprecated.ts | 22 - ...-overlay-iframe.service.deprecated.spec.ts | 521 ----- ...ofill-overlay-iframe.service.deprecated.ts | 429 ---- ...ill-overlay-list-iframe.deprecated.spec.ts | 26 - ...autofill-overlay-list-iframe.deprecated.ts | 26 - ...ill-overlay-button.deprecated.spec.ts.snap | 83 - ...autofill-overlay-button.deprecated.spec.ts | 135 -- .../autofill-overlay-button.deprecated.ts | 124 -- ...trap-autofill-overlay-button.deprecated.ts | 11 - .../overlay/pages/button/legacy-button.html | 12 - .../overlay/pages/button/legacy-button.scss | 36 - ...ofill-overlay-list.deprecated.spec.ts.snap | 537 ----- .../autofill-overlay-list.deprecated.spec.ts | 467 ----- .../list/autofill-overlay-list.deprecated.ts | 621 ------ ...tstrap-autofill-overlay-list.deprecated.ts | 11 - .../overlay/pages/list/legacy-list.html | 12 - .../overlay/pages/list/legacy-list.scss | 292 --- ...ll-overlay-page-element.deprecated.spec.ts | 222 --- ...utofill-overlay-page-element.deprecated.ts | 157 -- .../autofill-overlay-content.service.ts | 37 - ...overlay-content.service.deprecated.spec.ts | 1743 ----------------- ...fill-overlay-content.service.deprecated.ts | 1139 ----------- .../popup/settings/autofill.component.html | 10 +- .../popup/settings/autofill.component.ts | 17 +- .../src/autofill/services/autofill.service.ts | 9 - .../browser/src/background/main.background.ts | 56 +- apps/browser/src/manifest.json | 9 +- apps/browser/src/manifest.v3.json | 9 +- apps/browser/webpack.config.js | 16 - libs/common/src/enums/feature-flag.enum.ts | 2 - 46 files changed, 29 insertions(+), 10371 deletions(-) delete mode 100644 apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.html delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts delete mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index b07e06234d3..c093f1a3b00 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -1,7 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - import MainBackground from "../../background/main.background"; import { OverlayBackground } from "./abstractions/overlay.background"; @@ -14,7 +10,7 @@ export default class TabsBackground { private overlayBackground: OverlayBackground, ) {} - private focusedWindowId: number; + private focusedWindowId: number = -1; /** * Initializes the window and tab listeners. @@ -90,14 +86,6 @@ export default class TabsBackground { changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ) => { - const overlayImprovementsFlag = await this.main.configService.getFeatureFlag( - FeatureFlag.InlineMenuPositioningImprovements, - ); - const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { - this.overlayBackground.removePageDetails(tabId); - } - if (this.focusedWindowId > 0 && tab.windowId !== this.focusedWindowId) { return; } diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts deleted file mode 100644 index 88b78dc2495..00000000000 --- a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; - -import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background"; -import AutofillPageDetails from "../../../models/autofill-page-details"; - -type WebsiteIconData = { - imageEnabled: boolean; - image: string; - fallbackImage: string; - icon: string; -}; - -type OverlayAddNewItemMessage = { - login?: { - uri?: string; - hostname: string; - username: string; - password: string; - }; -}; - -type OverlayBackgroundExtensionMessage = { - [key: string]: any; - command: string; - tab?: chrome.tabs.Tab; - sender?: string; - details?: AutofillPageDetails; - overlayElement?: string; - display?: string; - data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; - -type OverlayPortMessage = { - [key: string]: any; - command: string; - direction?: string; - overlayCipherId?: string; -}; - -type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; -}; - -type OverlayCipherData = { - id: string; - name: string; - type: CipherType; - reprompt: CipherRepromptType; - favorite: boolean; - icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; - login?: { username: string }; - card?: string; -}; - -type BackgroundMessageParam = { - message: OverlayBackgroundExtensionMessage; -}; -type BackgroundSenderParam = { - sender: chrome.runtime.MessageSender; -}; -type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; - -type OverlayBackgroundExtensionMessageHandlers = { - [key: string]: CallableFunction; - openAutofillOverlay: () => void; - autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - getAutofillOverlayVisibility: () => void; - checkAutofillOverlayFocused: () => void; - focusAutofillOverlayList: () => void; - updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; - updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - unlockCompleted: ({ message }: BackgroundMessageParam) => void; - addedCipher: () => void; - addEditCipherSubmitted: () => void; - editedCipher: () => void; - deletedCipher: () => void; -}; - -type PortMessageParam = { - message: OverlayPortMessage; -}; -type PortConnectionParam = { - port: chrome.runtime.Port; -}; -type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; - -type OverlayButtonPortMessageHandlers = { - [key: string]: CallableFunction; - overlayButtonClicked: ({ port }: PortConnectionParam) => void; - closeAutofillOverlay: ({ port }: PortConnectionParam) => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; -}; - -type OverlayListPortMessageHandlers = { - [key: string]: CallableFunction; - checkAutofillOverlayButtonFocused: () => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - unlockVault: ({ port }: PortConnectionParam) => void; - fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; - addNewVaultItem: ({ port }: PortConnectionParam) => void; - viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; -}; - -export { - WebsiteIconData, - OverlayBackgroundExtensionMessage, - OverlayPortMessage, - FocusedFieldData, - OverlayCipherData, - OverlayAddNewItemMessage, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayListPortMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts deleted file mode 100644 index 68f8032350e..00000000000 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ /dev/null @@ -1,1464 +0,0 @@ -import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { - SHOW_AUTOFILL_BUTTON, - AutofillOverlayVisibility, -} from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { - DefaultDomainSettingsService, - DomainSettingsService, -} from "@bitwarden/common/autofill/services/domain-settings.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { - EnvironmentService, - Region, -} from "@bitwarden/common/platform/abstractions/environment.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { - FakeStateProvider, - FakeAccountService, - mockAccountServiceWith, -} from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; -import { - AutofillOverlayElement, - AutofillOverlayPort, - RedirectFocusDirection, -} from "../../enums/autofill-overlay.enum"; -import { AutofillService } from "../../services/abstractions/autofill.service"; -import { - createAutofillPageDetailsMock, - createChromeTabMock, - createFocusedFieldDataMock, - createPageDetailMock, - createPortSpyMock, -} from "../../spec/autofill-mocks"; -import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils"; - -import LegacyOverlayBackground from "./overlay.background.deprecated"; - -describe("OverlayBackground", () => { - const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); - let domainSettingsService: DomainSettingsService; - let buttonPortSpy: chrome.runtime.Port; - let listPortSpy: chrome.runtime.Port; - let overlayBackground: LegacyOverlayBackground; - const cipherService = mock(); - const autofillService = mock(); - let configService: MockProxy; - let activeAccountStatusMock$: BehaviorSubject; - let authService: MockProxy; - - const environmentService = mock(); - environmentService.environment$ = new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ); - const autofillSettingsService = mock(); - const i18nService = mock(); - const platformUtilsService = mock(); - const themeStateService = mock(); - const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { - const { initList, initButton } = options; - if (initButton) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - } - - if (initList) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); - listPortSpy = overlayBackground["overlayListPort"]; - } - - return { buttonPortSpy, listPortSpy }; - }; - - beforeEach(() => { - configService = mock(); - configService.getFeatureFlag$.mockImplementation(() => of(true)); - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); - activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); - authService = mock(); - authService.activeAccountStatus$ = activeAccountStatusMock$; - overlayBackground = new LegacyOverlayBackground( - cipherService, - autofillService, - authService, - environmentService, - domainSettingsService, - autofillSettingsService, - i18nService, - platformUtilsService, - themeStateService, - accountService, - ); - - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - - themeStateService.selectedTheme$ = of(ThemeType.Light); - domainSettingsService.showFavicons$ = of(true); - - void overlayBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - mockReset(cipherService); - }); - - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { - const tabId = 1; - const frameId = 2; - overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); - overlayBackground.removePageDetails(tabId); - - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); - }); - }); - - describe("init", () => { - it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { - overlayBackground["setupExtensionMessageListeners"] = jest.fn(); - overlayBackground["getOverlayVisibility"] = jest.fn(); - overlayBackground["getAuthStatus"] = jest.fn(); - - await overlayBackground.init(); - - expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - }); - }); - - describe("updateOverlayCiphers", () => { - const url = "https://jest-testing-website.com"; - const tab = createChromeTabMock({ url }); - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - - beforeEach(() => { - activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); - }); - - it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { - activeAccountStatusMock$.next(AuthenticationStatus.Locked); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); - }); - - it("ignores updating the overlay ciphers if the tab is undefined", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); - }); - - it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); - cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId); - expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); - expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( - new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ]), - ); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - }); - - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { - overlayBackground["overlayListPort"] = mock(); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); - cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - await overlayBackground.updateOverlayCiphers(); - - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayListCiphers", - ciphers: [ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - ], - }); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - tab, - "updateIsOverlayCiphersPopulated", - { isOverlayCiphersPopulated: true }, - ); - }); - }); - - describe("getOverlayCipherData", () => { - const url = "https://jest-testing-website.com"; - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - const cipher3 = mock({ - id: "id-3", - localData: { lastUsedDate: 333 }, - name: "name-3", - type: CipherType.Card, - card: { subTitle: "Visa, *6789" }, - }); - const cipher4 = mock({ - id: "id-4", - localData: { lastUsedDate: 444 }, - name: "name-4", - type: CipherType.Card, - card: { subTitle: "Mastercard, *1234" }, - }); - - it("formats and returns the cipher data", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher3], - ["overlay-cipher-3", cipher4], - ]); - - const overlayCipherData = await overlayBackground["getOverlayCipherData"](); - - expect(overlayCipherData).toStrictEqual([ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - { - card: "Visa, *6789", - favorite: cipher3.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: null, - imageEnabled: true, - }, - id: "overlay-cipher-2", - login: null, - name: "name-3", - reprompt: cipher3.reprompt, - type: 3, - }, - { - card: "Mastercard, *1234", - favorite: cipher4.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: null, - imageEnabled: true, - }, - id: "overlay-cipher-3", - login: null, - name: "name-4", - reprompt: cipher4.reprompt, - type: 3, - }, - ]); - }); - }); - - describe("getAuthStatus", () => { - it("will update the user's auth status but will not update the overlay ciphers", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - const status = await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - expect(status).toBe(authStatus); - }); - - it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - }); - }); - - describe("updateOverlayButtonAuthStatus", () => { - it("will send a message to the button port with the user's auth status", () => { - overlayBackground["overlayButtonPort"] = mock(); - jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); - - overlayBackground["updateOverlayButtonAuthStatus"](); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayButtonAuthStatus", - authStatus: overlayBackground["userAuthStatus"], - }); - }); - }); - - describe("getTranslations", () => { - it("will query the overlay page translations if they have not been queried", () => { - overlayBackground["overlayPageTranslations"] = undefined; - jest.spyOn(overlayBackground as any, "getTranslations"); - jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); - jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); - - const translations = overlayBackground["getTranslations"](); - - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - const translationKeys = [ - "opensInANewWindow", - "bitwardenOverlayButton", - "toggleBitwardenVaultOverlay", - "bitwardenVault", - "unlockYourAccountToViewMatchingLogins", - "unlockAccount", - "fillCredentialsFor", - "partialUsername", - "view", - "noItemsToShow", - "newItem", - "addNewVaultItem", - ]; - translationKeys.forEach((key) => { - expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); - }); - expect(translations).toStrictEqual({ - locale: "en", - opensInANewWindow: "opensInANewWindow", - buttonPageTitle: "bitwardenOverlayButton", - toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", - listPageTitle: "bitwardenVault", - unlockYourAccount: "unlockYourAccountToViewMatchingLogins", - unlockAccount: "unlockAccount", - fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", - view: "view", - noItemsToShow: "noItemsToShow", - newItem: "newItem", - addNewVaultItem: "addNewVaultItem", - }); - }); - }); - - describe("setupExtensionMessageListeners", () => { - it("will set up onMessage and onConnect listeners", () => { - overlayBackground["setupExtensionMessageListeners"](); - - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); - }); - }); - - describe("handleExtensionMessage", () => { - it("will return early if the message command is not present within the extensionMessageHandlers", () => { - const message = { - command: "not-a-command", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(null); - expect(sendResponse).not.toHaveBeenCalled(); - }); - - it("will trigger the message handler and return undefined if the message does not have a response", () => { - const message = { - command: "autofillOverlayElementClosed", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "overlayElementClosed"); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(null); - expect(sendResponse).not.toHaveBeenCalled(); - expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); - }); - - it("will return a response if the message handler returns a response", async () => { - const message = { - command: "openAutofillOverlay", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(true); - }); - - describe("extension message handlers", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockResolvedValue(AuthenticationStatus.Unlocked); - }); - - describe("openAutofillOverlay message handler", () => { - it("opens the autofill overlay by sending a message to the current tab", async () => { - const sender = mock({ tab: { id: 1 } }); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendMockExtensionMessage({ command: "openAutofillOverlay" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: false, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("autofillOverlayElementClosed message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { - const port1 = mock(); - const port2 = mock(); - overlayBackground["expiredPorts"] = [port1, port2]; - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage( - { - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }, - sender, - ); - - expect(port1.disconnect).toHaveBeenCalled(); - expect(port2.disconnect).toHaveBeenCalled(); - }); - - it("disconnects the button element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayButtonPort"]).toBeNull(); - }); - - it("disconnects the list element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayListPort"]).toBeNull(); - }); - }); - - describe("autofillOverlayAddNewVaultItem message handler", () => { - let sender: chrome.runtime.MessageSender; - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - jest - .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") - .mockImplementation(); - jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); - }); - - it("will not open the add edit popout window if the message does not have a login cipher provided", () => { - sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); - }); - - it("will open the add edit popout window after creating a new cipher", async () => { - jest.spyOn(BrowserApi, "sendMessage"); - - sendMockExtensionMessage( - { - command: "autofillOverlayAddNewVaultItem", - login: { - uri: "https://tacos.com", - hostname: "", - username: "username", - password: "password", - }, - }, - sender, - ); - await flushPromises(); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); - }); - }); - - describe("getAutofillOverlayVisibility message handler", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - }); - - it("will set the overlayVisibility property", async () => { - sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); - await flushPromises(); - - expect(await overlayBackground["getOverlayVisibility"]()).toBe( - AutofillOverlayVisibility.OnFieldFocus, - ); - }); - - it("returns the overlayVisibility property", async () => { - const sendMessageSpy = jest.fn(); - - sendMockExtensionMessage( - { command: "getAutofillOverlayVisibility" }, - undefined, - sendMessageSpy, - ); - await flushPromises(); - - expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); - }); - }); - - describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("will check if the overlay list is focused if the list port is open", () => { - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - }); - - it("will check if the overlay button is focused if the list port is not open", () => { - overlayBackground["overlayListPort"] = undefined; - - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - }); - }); - - describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - - sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); - }); - }); - - describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(async () => { - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.List), - ); - listPortSpy = overlayBackground["overlayListPort"]; - - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.Button), - ); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the overlay button's position", () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the overlay button's height for medium sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the overlay button's height for large sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("will post a message to the overlay list facilitating an update of the list's position", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - overlayBackground["updateOverlayPosition"]( - { overlayElement: AutofillOverlayElement.List }, - sender, - ); - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - }); - - describe("updateOverlayHidden", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the display value is not provided", () => { - const message = { - command: "updateAutofillOverlayHidden", - }; - - sendMockExtensionMessage(message); - - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); - }); - - it("posts a message to the overlay button and list with the display value", () => { - const message = { command: "updateAutofillOverlayHidden", display: "none" }; - - sendMockExtensionMessage(message); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - }); - }); - - describe("collectPageDetailsResponse message handler", () => { - let sender: chrome.runtime.MessageSender; - const pageDetails1 = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - const pageDetails2 = createAutofillPageDetailsMock({ - login: { username: "username2", password: "password2" }, - }); - - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - }); - - it("stores the page details provided by the message by the tab id of the sender", () => { - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails1 }, - sender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]), - ); - }); - - it("updates the page details for a tab that already has a set of page details stored ", () => { - const secondFrameSender = mock({ - tab: { id: 1 }, - frameId: 3, - }); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]); - - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails2 }, - secondFrameSender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - [ - secondFrameSender.frameId, - { - frameId: secondFrameSender.frameId, - tab: secondFrameSender.tab, - details: pageDetails2, - }, - ], - ]), - ); - }); - }); - - describe("unlockCompleted message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; - - beforeEach(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(BrowserApi, "tabSendMessageData"); - getAuthStatusSpy = jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockImplementation(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - return Promise.resolve(AuthenticationStatus.Unlocked); - }); - }); - - it("updates the user's auth status but does not open the overlay", async () => { - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "" } }, - }, - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { - const sender = mock({ tab: { id: 1 } }); - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "openAutofillOverlay" } }, - }, - }; - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: true, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("extension messages that trigger an update of the inline menu ciphers", () => { - const extensionMessages = [ - "addedCipher", - "addEditCipherSubmitted", - "editedCipher", - "deletedCipher", - ]; - - beforeEach(() => { - jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); - }); - - extensionMessages.forEach((message) => { - it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { - sendMockExtensionMessage({ command: message }); - expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); - }); - }); - }); - }); - }); - - describe("handlePortOnConnect", () => { - beforeEach(() => { - jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); - jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); - }); - - it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { - const port = createPortSpyMock("not-an-overlay-element"); - - await overlayBackground["handlePortOnConnect"](port); - - expect(port.onMessage.addListener).not.toHaveBeenCalled(); - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("sets up the overlay list port if the port connection is for the overlay list", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - await flushPromises(); - - expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); - expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(listPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.List }, - listPortSpy.sender, - ); - }); - - it("sets up the overlay button port if the port connection is for the overlay button", async () => { - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["overlayListPort"]).toBeUndefined(); - expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(buttonPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.Button }, - buttonPortSpy.sender, - ); - }); - - it("stores an existing overlay port so that it can be disconnected at a later time", async () => { - overlayBackground["overlayButtonPort"] = mock(); - - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["expiredPorts"].length).toBe(1); - }); - - it("gets the system theme", async () => { - themeStateService.selectedTheme$ = of(ThemeType.System); - - await initOverlayElementPorts({ initList: true, initButton: false }); - await flushPromises(); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ theme: ThemeType.System }), - ); - }); - }); - - describe("handleOverlayElementPortMessage", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - }); - - it("ignores port messages that do not contain a handler", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); - }); - - describe("overlay button message handlers", () => { - it("unlocks the vault if the user auth status is not unlocked", () => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); - }); - - it("opens the autofill overlay if the auth status is unlocked", () => { - jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); - }); - - describe("closeAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: false }, - ); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks if the overlay list is focused", () => { - jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); - - sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - beforeEach(() => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - }); - - it("ignores the redirect message if the direction is not provided", () => { - sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); - - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("sends the redirect message if the direction is provided", () => { - sendPortMessage(buttonPortSpy, { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "redirectOverlayFocusOut", - { direction: RedirectFocusDirection.Next }, - ); - }); - }); - }); - - describe("overlay list message handlers", () => { - describe("checkAutofillOverlayButtonFocused", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("unlockVault", () => { - it("closes the autofill overlay and opens the unlock popout", async () => { - jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); - jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "unlockVault" }); - await flushPromises(); - - expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "addToLockedVaultPendingNotifications", - { - commandToRetry: { - message: { command: "openAutofillOverlay" }, - sender: listPortSpy.sender, - }, - target: "overlay.background", - }, - ); - expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - true, - ); - }); - }); - - describe("fillSelectedListItem", () => { - let getLoginCiphersSpy: jest.SpyInstance; - let isPasswordRepromptRequiredSpy: jest.SpyInstance; - let doAutoFillSpy: jest.SpyInstance; - let sender: chrome.runtime.MessageSender; - const pageDetails = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - - beforeEach(() => { - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy = jest.spyOn( - overlayBackground["autofillService"], - "isPasswordRepromptRequired", - ); - doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); - sender = mock({ tab: { id: 1 } }); - }); - - it("ignores the fill request if the overlay cipher id is not provided", async () => { - sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if the tab does not contain any identified page details", async () => { - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if a master password reprompt is required", async () => { - const cipher = mock({ - reprompt: CipherRepromptType.Password, - type: CipherType.Login, - }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy.mockResolvedValue(true); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - const cipher2 = mock({ id: "overlay-cipher-2" }); - const cipher3 = mock({ id: "overlay-cipher-3" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher2], - ["overlay-cipher-3", cipher3], - ]); - const pageDetailsForTab = { - frameId: sender.frameId, - tab: sender.tab, - details: pageDetails, - }; - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, pageDetailsForTab], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher2, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).toHaveBeenCalledWith({ - tab: listPortSpy.sender.tab, - cipher: cipher2, - pageDetails: [pageDetailsForTab], - fillNewPassword: true, - allowTotpAutofill: true, - }); - expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( - new Map([ - ["overlay-cipher-2", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-3", cipher3], - ]).entries(), - ); - }); - - it("copies the cipher's totp code to the clipboard after filling", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") - .mockImplementation(); - doAutoFillSpy.mockReturnValueOnce("totp-code"); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); - }); - }); - - describe("getNewVaultItemDetails", () => { - it("will send an addNewVaultItemFromOverlay message", async () => { - jest.spyOn(BrowserApi, "tabSendMessage"); - - sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { - command: "addNewVaultItemFromOverlay", - }); - }); - }); - - describe("viewSelectedCipher", () => { - let openViewVaultItemPopoutSpy: jest.SpyInstance; - - beforeEach(() => { - openViewVaultItemPopoutSpy = jest - .spyOn(overlayBackground as any, "openViewVaultItemPopout") - .mockImplementation(); - }); - - it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); - }); - - it("will open the view vault item popout with the selected cipher", async () => { - const cipher = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ["overlay-cipher-1", cipher], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }, - ); - }); - }); - - describe("redirectOverlayFocusOut", () => { - it("redirects focus out of the overlay list", async () => { - const message = { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }; - const redirectOverlayFocusOutSpy = jest.spyOn( - overlayBackground as any, - "redirectOverlayFocusOut", - ); - - sendPortMessage(listPortSpy, message); - await flushPromises(); - - expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); - }); - }); - }); - }); -}); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts deleted file mode 100644 index c9eb442d75d..00000000000 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts +++ /dev/null @@ -1,811 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; - -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; - -import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window"; -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { - openViewVaultItemPopout, - openAddEditVaultItemPopout, -} from "../../../vault/popup/utils/vault-popout-window"; -import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background"; -import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum"; -import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service"; - -import { - FocusedFieldData, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayCipherData, - OverlayListPortMessageHandlers, - OverlayBackgroundExtensionMessage, - OverlayAddNewItemMessage, - OverlayPortMessage, - WebsiteIconData, -} from "./abstractions/overlay.background.deprecated"; - -class LegacyOverlayBackground implements OverlayBackgroundInterface { - private readonly openUnlockPopout = openUnlockPopout; - private readonly openViewVaultItemPopout = openViewVaultItemPopout; - private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayLoginCiphers: Map = new Map(); - private pageDetailsForTab: Record< - chrome.runtime.MessageSender["tab"]["id"], - Map - > = {}; - private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; - private overlayButtonPort: chrome.runtime.Port; - private overlayListPort: chrome.runtime.Port; - private expiredPorts: chrome.runtime.Port[] = []; - private focusedFieldData: FocusedFieldData; - private overlayPageTranslations: Record; - private iconsServerUrl: string; - private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { - openAutofillOverlay: () => this.openOverlay(false), - autofillOverlayElementClosed: ({ message, sender }) => - this.overlayElementClosed(message, sender), - autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), - getAutofillOverlayVisibility: () => this.getOverlayVisibility(), - checkAutofillOverlayFocused: () => this.checkOverlayFocused(), - focusAutofillOverlayList: () => this.focusOverlayList(), - updateAutofillOverlayPosition: ({ message, sender }) => - this.updateOverlayPosition(message, sender), - updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), - updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), - collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), - unlockCompleted: ({ message }) => this.unlockCompleted(message), - addedCipher: () => this.updateOverlayCiphers(), - addEditCipherSubmitted: () => this.updateOverlayCiphers(), - editedCipher: () => this.updateOverlayCiphers(), - deletedCipher: () => this.updateOverlayCiphers(), - }; - private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { - overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), - closeAutofillOverlay: ({ port }) => this.closeOverlay(port), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayListFocused(), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), - }; - private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { - checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayButtonFocused(), - unlockVault: ({ port }) => this.unlockVault(port), - fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), - addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), - viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), - }; - - constructor( - private cipherService: CipherService, - private autofillService: AutofillService, - private authService: AuthService, - private environmentService: EnvironmentService, - private domainSettingsService: DomainSettingsService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private themeStateService: ThemeStateService, - private accountService: AccountService, - ) {} - - /** - * Removes cached page details for a tab - * based on the passed tabId. - * - * @param tabId - Used to reference the page details of a specific tab - */ - removePageDetails(tabId: number) { - if (!this.pageDetailsForTab[tabId]) { - return; - } - - this.pageDetailsForTab[tabId].clear(); - delete this.pageDetailsForTab[tabId]; - } - - /** - * Sets up the extension message listeners and gets the settings for the - * overlay's visibility and the user's authentication status. - */ - async init() { - this.setupExtensionMessageListeners(); - const env = await firstValueFrom(this.environmentService.environment$); - this.iconsServerUrl = env.getIconsUrl(); - await this.getOverlayVisibility(); - await this.getAuthStatus(); - } - - /** - * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. - * Queries all ciphers for the given url, and sorts them by last used. Will not update the - * list of ciphers if the extension is not unlocked. - */ - async updateOverlayCiphers() { - const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); - if (authStatus !== AuthenticationStatus.Unlocked) { - return; - } - - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab?.url) { - return; - } - - this.overlayLoginCiphers = new Map(); - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const ciphersViews = ( - await this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId) - ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); - for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { - this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); - } - - const ciphers = await this.getOverlayCipherData(); - this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); - await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { - isOverlayCiphersPopulated: Boolean(ciphers.length), - }); - } - - /** - * Strips out unnecessary data from the ciphers and returns an array of - * objects that contain the cipher data needed for the overlay list. - */ - private async getOverlayCipherData(): Promise { - const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); - const overlayCiphersArray = Array.from(this.overlayLoginCiphers); - const overlayCipherData: OverlayCipherData[] = []; - let loginCipherIcon: WebsiteIconData; - - for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { - const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; - if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); - } - - overlayCipherData.push({ - id: overlayCipherId, - name: cipher.name, - type: cipher.type, - reprompt: cipher.reprompt, - favorite: cipher.favorite, - icon: - cipher.type === CipherType.Login - ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), - login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, - card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, - }); - } - - return overlayCipherData; - } - - /** - * Handles aggregation of page details for a tab. Stores the page details - * in association with the tabId of the tab that sent the message. - * - * @param message - Message received from the `collectPageDetailsResponse` command - * @param sender - The sender of the message - */ - private storePageDetails( - message: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - const pageDetails = { - frameId: sender.frameId, - tab: sender.tab, - details: message.details, - }; - - const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; - if (!pageDetailsMap) { - this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); - return; - } - - pageDetailsMap.set(sender.frameId, pageDetails); - } - - /** - * Triggers autofill for the selected cipher in the overlay list. Also places - * the selected cipher at the top of the list of ciphers. - * - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message - */ - private async fillSelectedOverlayListItem( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!overlayCipherId || !pageDetails?.size) { - return; - } - - const cipher = this.overlayLoginCiphers.get(overlayCipherId); - - if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { - return; - } - const totpCode = await this.autofillService.doAutoFill({ - tab: sender.tab, - cipher: cipher, - pageDetails: Array.from(pageDetails.values()), - fillNewPassword: true, - allowTotpAutofill: true, - }); - - if (totpCode) { - this.platformUtilsService.copyToClipboard(totpCode); - } - - this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); - } - - /** - * Checks if the overlay is focused. Will check the overlay list - * if it is open, otherwise it will check the overlay button. - */ - private checkOverlayFocused() { - if (this.overlayListPort) { - this.checkOverlayListFocused(); - - return; - } - - this.checkOverlayButtonFocused(); - } - - /** - * Posts a message to the overlay button iframe to check if it is focused. - */ - private checkOverlayButtonFocused() { - this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); - } - - /** - * Posts a message to the overlay list iframe to check if it is focused. - */ - private checkOverlayListFocused() { - this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); - } - - /** - * Sends a message to the sender tab to close the autofill overlay. - * - * @param sender - The sender of the port message - * @param forceCloseOverlay - Identifies whether the overlay should be force closed - */ - private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); - } - - /** - * Handles cleanup when an overlay element is closed. Disconnects - * the list and button ports and sets them to null. - * - * @param overlayElement - The overlay element that was closed, either the list or button - * @param sender - The sender of the port message - */ - private overlayElementClosed( - { overlayElement }: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { - this.expiredPorts.forEach((port) => port.disconnect()); - this.expiredPorts = []; - return; - } - - if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.disconnect(); - this.overlayButtonPort = null; - - return; - } - - this.overlayListPort?.disconnect(); - this.overlayListPort = null; - } - - /** - * Updates the position of either the overlay list or button. The position - * is based on the focused field's position and dimensions. - * - * @param overlayElement - The overlay element to update, either the list or button - * @param sender - The sender of the port message - */ - private updateOverlayPosition( - { overlayElement }: { overlayElement?: string }, - sender: chrome.runtime.MessageSender, - ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { - return; - } - - if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayButtonPosition(), - }); - - return; - } - - this.overlayListPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayListPosition(), - }); - } - - /** - * Gets the position of the focused field and calculates the position - * of the overlay button based on the focused field's position and dimensions. - */ - private getOverlayButtonPosition() { - if (!this.focusedFieldData) { - return; - } - - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; - const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; - let elementOffset = height * 0.37; - if (height >= 35) { - elementOffset = height >= 50 ? height * 0.47 : height * 0.42; - } - - const elementHeight = height - elementOffset; - const elementTopPosition = top + elementOffset / 2; - let elementLeftPosition = left + width - height + elementOffset / 2; - - const fieldPaddingRight = parseInt(paddingRight, 10); - const fieldPaddingLeft = parseInt(paddingLeft, 10); - if (fieldPaddingRight > fieldPaddingLeft) { - elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); - } - - return { - top: `${Math.round(elementTopPosition)}px`, - left: `${Math.round(elementLeftPosition)}px`, - height: `${Math.round(elementHeight)}px`, - width: `${Math.round(elementHeight)}px`, - }; - } - - /** - * Gets the position of the focused field and calculates the position - * of the overlay list based on the focused field's position and dimensions. - */ - private getOverlayListPosition() { - if (!this.focusedFieldData) { - return; - } - - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; - return { - width: `${Math.round(width)}px`, - top: `${Math.round(top + height)}px`, - left: `${Math.round(left)}px`, - }; - } - - /** - * Sets the focused field data to the data passed in the extension message. - * - * @param focusedFieldData - Contains the rects and styles of the focused field. - * @param sender - The sender of the extension message - */ - private setFocusedFieldData( - { focusedFieldData }: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; - } - - /** - * Updates the overlay's visibility based on the display property passed in the extension message. - * - * @param display - The display property of the overlay, either "block" or "none" - */ - private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { - if (!display) { - return; - } - - const portMessage = { command: "updateOverlayHidden", styles: { display } }; - - this.overlayButtonPort?.postMessage(portMessage); - this.overlayListPort?.postMessage(portMessage); - } - - /** - * Sends a message to the currently active tab to open the autofill overlay. - * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened - * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states - */ - private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - - await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { - isFocusingFieldElement, - isOpeningFullOverlay, - authStatus: await this.getAuthStatus(), - }); - } - - /** - * Gets the overlay's visibility setting from the settings service. - */ - private async getOverlayVisibility(): Promise { - return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); - } - - /** - * Gets the user's authentication status from the auth service. If the user's - * authentication status has changed, the overlay button's authentication status - * will be updated and the overlay list's ciphers will be updated. - */ - private async getAuthStatus() { - const formerAuthStatus = this.userAuthStatus; - this.userAuthStatus = await this.authService.getAuthStatus(); - - if ( - this.userAuthStatus !== formerAuthStatus && - this.userAuthStatus === AuthenticationStatus.Unlocked - ) { - this.updateOverlayButtonAuthStatus(); - await this.updateOverlayCiphers(); - } - - return this.userAuthStatus; - } - - /** - * Sends a message to the overlay button to update its authentication status. - */ - private updateOverlayButtonAuthStatus() { - this.overlayButtonPort?.postMessage({ - command: "updateOverlayButtonAuthStatus", - authStatus: this.userAuthStatus, - }); - } - - /** - * Handles the overlay button being clicked. If the user is not authenticated, - * the vault will be unlocked. If the user is authenticated, the overlay will - * be opened. - * - * @param port - The port of the overlay button - */ - private handleOverlayButtonClicked(port: chrome.runtime.Port) { - if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.unlockVault(port); - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.openOverlay(false, true); - } - - /** - * Facilitates opening the unlock popout window. - * - * @param port - The port of the overlay list - */ - private async unlockVault(port: chrome.runtime.Port) { - const { sender } = port; - - this.closeOverlay(port); - const retryMessage: LockedVaultPendingNotificationsData = { - commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, - target: "overlay.background", - }; - await BrowserApi.tabSendMessageData( - sender.tab, - "addToLockedVaultPendingNotifications", - retryMessage, - ); - await this.openUnlockPopout(sender.tab, true); - } - - /** - * Triggers the opening of a vault item popout window associated - * with the passed cipher ID. - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message - */ - private async viewSelectedCipher( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - const cipher = this.overlayLoginCiphers.get(overlayCipherId); - if (!cipher) { - return; - } - - await this.openViewVaultItemPopout(sender.tab, { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }); - } - - /** - * Facilitates redirecting focus to the overlay list. - */ - private focusOverlayList() { - this.overlayListPort?.postMessage({ command: "focusOverlayList" }); - } - - /** - * Updates the authentication status for the user and opens the overlay if - * a followup command is present in the message. - * - * @param message - Extension message received from the `unlockCompleted` command - */ - private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { - await this.getAuthStatus(); - - if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { - await this.openOverlay(true); - } - } - - /** - * Gets the translations for the overlay page. - */ - private getTranslations() { - if (!this.overlayPageTranslations) { - this.overlayPageTranslations = { - locale: BrowserApi.getUILanguage(), - opensInANewWindow: this.i18nService.translate("opensInANewWindow"), - buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), - toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), - listPageTitle: this.i18nService.translate("bitwardenVault"), - unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), - unlockAccount: this.i18nService.translate("unlockAccount"), - fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - partialUsername: this.i18nService.translate("partialUsername"), - view: this.i18nService.translate("view"), - noItemsToShow: this.i18nService.translate("noItemsToShow"), - newItem: this.i18nService.translate("newItem"), - addNewVaultItem: this.i18nService.translate("addNewVaultItem"), - }; - } - - return this.overlayPageTranslations; - } - - /** - * Facilitates redirecting focus out of one of the - * overlay elements to elements on the page. - * - * @param direction - The direction to redirect focus to (either "next", "previous" or "current) - * @param sender - The sender of the port message - */ - private redirectOverlayFocusOut( - { direction }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - if (!direction) { - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); - } - - /** - * Triggers adding a new vault item from the overlay. Gathers data - * input by the user before calling to open the add/edit window. - * - * @param sender - The sender of the port message - */ - private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); - } - - /** - * Handles adding a new vault item from the overlay. Gathers data login - * data captured in the extension message. - * - * @param login - The login data captured from the extension message - * @param sender - The sender of the extension message - */ - private async addNewVaultItem( - { login }: OverlayAddNewItemMessage, - sender: chrome.runtime.MessageSender, - ) { - if (!login) { - return; - } - - const uriView = new LoginUriView(); - uriView.uri = login.uri; - - const loginView = new LoginView(); - loginView.uris = [uriView]; - loginView.username = login.username || ""; - loginView.password = login.password || ""; - - const cipherView = new CipherView(); - cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, ""); - cipherView.folderId = null; - cipherView.type = CipherType.Login; - cipherView.login = loginView; - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - await this.cipherService.setAddEditCipherInfo( - { - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }, - activeUserId, - ); - - await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); - } - - /** - * Sets up the extension message listeners for the overlay. - */ - private setupExtensionMessageListeners() { - BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); - BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); - } - - /** - * Handles extension messages sent to the extension background. - * - * @param message - The message received from the extension - * @param sender - The sender of the message - * @param sendResponse - The response to send back to the sender - */ - private handleExtensionMessage = ( - message: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - sendResponse: (response?: any) => void, - ) => { - const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; - if (!handler) { - return null; - } - - const messageResponse = handler({ message, sender }); - if (typeof messageResponse === "undefined") { - return null; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; - }; - - /** - * Handles the connection of a port to the extension background. - * - * @param port - The port that connected to the extension background - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isOverlayListPort = port.name === AutofillOverlayPort.List; - const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; - if (!isOverlayListPort && !isOverlayButtonPort) { - return; - } - - this.storeOverlayPort(port); - port.onMessage.addListener(this.handleOverlayElementPortMessage); - port.postMessage({ - command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, - authStatus: await this.getAuthStatus(), - styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), - theme: await firstValueFrom(this.themeStateService.selectedTheme$), - translations: this.getTranslations(), - ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, - }); - this.updateOverlayPosition( - { - overlayElement: isOverlayListPort - ? AutofillOverlayElement.List - : AutofillOverlayElement.Button, - }, - port.sender, - ); - }; - - /** - * Stores the connected overlay port and sets up any existing ports to be disconnected. - * - * @param port - The port to store -| */ - private storeOverlayPort(port: chrome.runtime.Port) { - if (port.name === AutofillOverlayPort.List) { - this.storeExpiredOverlayPort(this.overlayListPort); - this.overlayListPort = port; - return; - } - - if (port.name === AutofillOverlayPort.Button) { - this.storeExpiredOverlayPort(this.overlayButtonPort); - this.overlayButtonPort = port; - } - } - - /** - * When registering a new connection, we want to ensure that the port is disconnected. - * This method places an existing port in the expiredPorts array to be disconnected - * at a later time. - * - * @param port - The port to store in the expiredPorts array - */ - private storeExpiredOverlayPort(port: chrome.runtime.Port | null) { - if (port) { - this.expiredPorts.push(port); - } - } - - /** - * Handles messages sent to the overlay list or button ports. - * - * @param message - The message received from the port - * @param port - The port that sent the message - */ - private handleOverlayElementPortMessage = ( - message: OverlayBackgroundExtensionMessage, - port: chrome.runtime.Port, - ) => { - const command = message?.command; - let handler: CallableFunction | undefined; - - if (port.name === AutofillOverlayPort.Button) { - handler = this.overlayButtonPortMessageHandlers[command]; - } - - if (port.name === AutofillOverlayPort.List) { - handler = this.overlayListPortMessageHandlers[command]; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -export default LegacyOverlayBackground; diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts deleted file mode 100644 index ed422822b36..00000000000 --- a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -import AutofillScript from "../../../models/autofill-script"; - -type AutofillExtensionMessage = { - command: string; - tab?: chrome.tabs.Tab; - sender?: string; - fillScript?: AutofillScript; - url?: string; - pageDetailsUrl?: string; - ciphers?: any; - data?: { - authStatus?: AuthenticationStatus; - isFocusingFieldElement?: boolean; - isOverlayCiphersPopulated?: boolean; - direction?: "previous" | "next"; - isOpeningFullOverlay?: boolean; - forceCloseOverlay?: boolean; - autofillOverlayVisibility?: number; - }; -}; - -type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; - -type AutofillExtensionMessageHandlers = { - [key: string]: CallableFunction; - collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; - collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; - fillForm: ({ message }: AutofillExtensionMessageParam) => void; - openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - addNewVaultItemFromOverlay: () => void; - redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; - updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; - bgUnlockPopoutOpened: () => void; - bgVaultItemRepromptPopoutOpened: () => void; - updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; -}; - -export { AutofillExtensionMessage, AutofillExtensionMessageHandlers }; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts deleted file mode 100644 index 96d5e85ca34..00000000000 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; - -import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum"; -import AutofillPageDetails from "../../models/autofill-page-details"; -import AutofillScript from "../../models/autofill-script"; -import { - flushPromises, - mockQuerySelectorAllDefinedCall, - sendMockExtensionMessage, -} from "../../spec/testing-utils"; -import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated"; - -import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated"; -import AutofillInitDeprecated from "./autofill-init.deprecated"; - -describe("AutofillInit", () => { - let autofillInit: AutofillInitDeprecated; - const autofillOverlayContentService = mock(); - const originalDocumentReadyState = document.readyState; - const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); - - beforeEach(() => { - chrome.runtime.connect = jest.fn().mockReturnValue({ - onDisconnect: { - addListener: jest.fn(), - }, - }); - autofillInit = new AutofillInitDeprecated(autofillOverlayContentService); - window.IntersectionObserver = jest.fn(() => mock()); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - Object.defineProperty(document, "readyState", { - value: originalDocumentReadyState, - writable: true, - }); - }); - - afterAll(() => { - mockQuerySelectorAll.mockRestore(); - }); - - describe("init", () => { - it("sets up the extension message listeners", () => { - jest.spyOn(autofillInit as any, "setupExtensionMessageListeners"); - - autofillInit.init(); - - expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled(); - }); - - it("triggers a collection of page details if the document is in a `complete` ready state", () => { - jest.useFakeTimers(); - Object.defineProperty(document, "readyState", { value: "complete", writable: true }); - - autofillInit.init(); - jest.advanceTimersByTime(250); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( - { - command: "bgCollectPageDetails", - sender: "autofillInit", - }, - expect.any(Function), - ); - }); - - it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { - jest.spyOn(window, "addEventListener"); - Object.defineProperty(document, "readyState", { value: "loading", writable: true }); - - autofillInit.init(); - - expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); - }); - }); - - describe("setupExtensionMessageListeners", () => { - it("sets up a chrome runtime on message listener", () => { - jest.spyOn(chrome.runtime.onMessage, "addListener"); - - autofillInit["setupExtensionMessageListeners"](); - - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( - autofillInit["handleExtensionMessage"], - ); - }); - }); - - describe("handleExtensionMessage", () => { - let message: AutofillExtensionMessage; - let sender: chrome.runtime.MessageSender; - const sendResponse = jest.fn(); - - beforeEach(() => { - message = { - command: "collectPageDetails", - tab: mock(), - sender: "sender", - }; - sender = mock(); - }); - - it("returns a undefined value if a extension message handler is not found with the given message command", () => { - message.command = "unknownCommand"; - - const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - - expect(response).toBe(null); - }); - - it("returns a undefined value if the message handler does not return a response", async () => { - const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response1).not.toBe(false); - - message.command = "removeAutofillOverlay"; - message.fillScript = mock(); - - const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response2).toBe(null); - }); - - it("returns a true value and calls sendResponse if the message handler returns a response", async () => { - message.command = "collectPageDetailsImmediately"; - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response).toBe(true); - expect(sendResponse).toHaveBeenCalledWith(pageDetails); - }); - - describe("extension message handlers", () => { - beforeEach(() => { - autofillInit.init(); - }); - - describe("collectPageDetails", () => { - it("sends the collected page details for autofill using a background script message", async () => { - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - const message = { - command: "collectPageDetails", - sender: "sender", - tab: mock(), - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - sendMockExtensionMessage(message, sender, sendResponse); - await flushPromises(); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - }); - }); - - describe("collectPageDetailsImmediately", () => { - it("returns collected page details for autofill if set to send the details in the response", async () => { - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - sendMockExtensionMessage( - { command: "collectPageDetailsImmediately" }, - sender, - sendResponse, - ); - await flushPromises(); - - expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled(); - expect(sendResponse).toBeCalledWith(pageDetails); - expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - }); - }); - - describe("fillForm", () => { - let fillScript: AutofillScript; - beforeEach(() => { - fillScript = mock(); - jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation(); - }); - - it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { - const fillScript = mock(); - const message = { - command: "fillForm", - fillScript, - pageDetailsUrl: "https://a-different-url.com", - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( - fillScript, - ); - }); - - it("calls the InsertAutofillContentService to fill the form", async () => { - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - - expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - }); - - it("removes the overlay when filling the form", async () => { - const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - - expect(blurAndRemoveOverlaySpy).toHaveBeenCalled(); - }); - - it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { - jest.useFakeTimers(); - jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); - expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); - }); - - it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { - jest.useFakeTimers(); - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( - 1, - true, - ); - expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( - 2, - false, - ); - }); - }); - - describe("openAutofillOverlay", () => { - const message = { - command: "openAutofillOverlay", - data: { - isFocusingFieldElement: true, - isOpeningFullOverlay: true, - authStatus: AuthenticationStatus.Unlocked, - }, - }; - - it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("opens the autofill overlay", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].openAutofillOverlay, - ).toHaveBeenCalledWith({ - isFocusingFieldElement: message.data.isFocusingFieldElement, - isOpeningFullOverlay: message.data.isOpeningFullOverlay, - authStatus: message.data.authStatus, - }); - }); - }); - - describe("closeAutofillOverlay", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; - }); - - it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: false }, - }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("removes the autofill overlay if the message flags a forced closure", () => { - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: true }, - }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - - it("ignores the message if a field is currently focused", () => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the autofill overlay list if the overlay is currently filling", () => { - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the entire overlay if the overlay is not currently filling", () => { - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - }); - - describe("addNewVaultItemFromOverlay", () => { - it("will not add a new vault item if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("will add a new vault item", () => { - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - const message = { - command: "redirectOverlayFocusOut", - data: { - direction: RedirectFocusDirection.Next, - }, - }; - - it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("redirects the overlay focus", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, - ).toHaveBeenCalledWith(message.data.direction); - }); - }); - - describe("updateIsOverlayCiphersPopulated", () => { - const message = { - command: "updateIsOverlayCiphersPopulated", - data: { - isOverlayCiphersPopulated: true, - }, - }; - - it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("updates whether the overlay ciphers are populated", () => { - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( - message.data.isOverlayCiphersPopulated, - ); - }); - }); - - describe("bgUnlockPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("bgVaultItemRepromptPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("updateAutofillOverlayVisibility", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = - AutofillOverlayVisibility.OnButtonClick; - }); - - it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { - sendMockExtensionMessage({ - command: "updateAutofillOverlayVisibility", - data: {}, - }); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - AutofillOverlayVisibility.OnButtonClick, - ); - }); - - it("updates the overlay visibility value", () => { - const message = { - command: "updateAutofillOverlayVisibility", - data: { - autofillOverlayVisibility: AutofillOverlayVisibility.Off, - }, - }; - - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - message.data.autofillOverlayVisibility, - ); - }); - }); - }); - }); - - describe("destroy", () => { - it("clears the timeout used to collect page details on load", () => { - jest.spyOn(window, "clearTimeout"); - - autofillInit.init(); - autofillInit.destroy(); - - expect(window.clearTimeout).toHaveBeenCalledWith( - autofillInit["collectPageDetailsOnLoadTimeout"], - ); - }); - - it("removes the extension message listeners", () => { - autofillInit.destroy(); - - expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith( - autofillInit["handleExtensionMessage"], - ); - }); - - it("destroys the collectAutofillContentService", () => { - jest.spyOn(autofillInit["collectAutofillContentService"], "destroy"); - - autofillInit.destroy(); - - expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts deleted file mode 100644 index fac9c0852f5..00000000000 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts +++ /dev/null @@ -1,315 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { AutofillInit } from "../../content/abstractions/autofill-init"; -import AutofillPageDetails from "../../models/autofill-page-details"; -import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; -import DomElementVisibilityService from "../../services/dom-element-visibility.service"; -import { DomQueryService } from "../../services/dom-query.service"; -import InsertAutofillContentService from "../../services/insert-autofill-content.service"; -import { sendExtensionMessage } from "../../utils"; -import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; - -import { - AutofillExtensionMessage, - AutofillExtensionMessageHandlers, -} from "./abstractions/autofill-init.deprecated"; - -class LegacyAutofillInit implements AutofillInit { - private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined; - private readonly domElementVisibilityService: DomElementVisibilityService; - private readonly collectAutofillContentService: CollectAutofillContentService; - private readonly insertAutofillContentService: InsertAutofillContentService; - private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; - private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { - collectPageDetails: ({ message }) => this.collectPageDetails(message), - collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), - fillForm: ({ message }) => this.fillForm(message), - openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), - closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), - addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), - redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), - updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), - bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), - bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), - updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), - }; - - /** - * AutofillInit constructor. Initializes the DomElementVisibilityService, - * CollectAutofillContentService and InsertAutofillContentService classes. - * - * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. - */ - constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { - this.autofillOverlayContentService = autofillOverlayContentService; - this.domElementVisibilityService = new DomElementVisibilityService(); - const domQueryService = new DomQueryService(); - this.collectAutofillContentService = new CollectAutofillContentService( - this.domElementVisibilityService, - domQueryService, - this.autofillOverlayContentService, - ); - this.insertAutofillContentService = new InsertAutofillContentService( - this.domElementVisibilityService, - this.collectAutofillContentService, - ); - } - - /** - * Initializes the autofill content script, setting up - * the extension message listeners. This method should - * be called once when the content script is loaded. - */ - init() { - this.setupExtensionMessageListeners(); - this.autofillOverlayContentService?.init(); - this.collectPageDetailsOnLoad(); - } - - /** - * Triggers a collection of the page details from the - * background script, ensuring that autofill is ready - * to act on the page. - */ - private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => { - this.clearCollectPageDetailsOnLoadTimeout(); - this.collectPageDetailsOnLoadTimeout = setTimeout( - () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), - 250, - ); - }; - - if (globalThis.document.readyState === "complete") { - sendCollectDetailsMessage(); - } - - globalThis.addEventListener("load", sendCollectDetailsMessage); - } - - /** - * Collects the page details and sends them to the - * extension background script. If the `sendDetailsInResponse` - * parameter is set to true, the page details will be - * returned to facilitate sending the details in the - * response to the extension message. - * - * @param message - The extension message. - * @param sendDetailsInResponse - Determines whether to send the details in the response. - */ - private async collectPageDetails( - message: AutofillExtensionMessage, - sendDetailsInResponse = false, - ): Promise { - const pageDetails: AutofillPageDetails = - await this.collectAutofillContentService.getPageDetails(); - if (sendDetailsInResponse) { - return pageDetails; - } - - void chrome.runtime.sendMessage({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - } - - /** - * Fills the form with the given fill script. - * - * @param {AutofillExtensionMessage} message - */ - private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { - if ((document.defaultView || window).location.href !== pageDetailsUrl) { - return; - } - - this.blurAndRemoveOverlay(); - this.updateOverlayIsCurrentlyFilling(true); - await this.insertAutofillContentService.fillForm(fillScript); - - if (!this.autofillOverlayContentService) { - return; - } - - setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); - } - - /** - * Handles updating the overlay is currently filling value. - * - * @param isCurrentlyFilling - Indicates if the overlay is currently filling - */ - private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; - } - - /** - * Opens the autofill overlay. - * - * @param data - The extension message data. - */ - private openAutofillOverlay({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.openAutofillOverlay(data); - } - - /** - * Blurs the most recent overlay field and removes the overlay. Used - * in cases where the background unlock or vault item reprompt popout - * is opened. - */ - private blurAndRemoveOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - } - - /** - * Removes the autofill overlay if the field is not currently focused. - * If the autofill is currently filling, only the overlay list will be - * removed. - */ - private removeAutofillOverlay(message?: AutofillExtensionMessage) { - if (message?.data?.forceCloseOverlay) { - this.autofillOverlayContentService?.removeAutofillOverlay(); - return; - } - - if ( - !this.autofillOverlayContentService || - this.autofillOverlayContentService.isFieldCurrentlyFocused - ) { - return; - } - - if (this.autofillOverlayContentService.isCurrentlyFilling) { - this.autofillOverlayContentService.removeAutofillOverlayList(); - return; - } - - this.autofillOverlayContentService.removeAutofillOverlay(); - } - - /** - * Adds a new vault item from the overlay. - */ - private addNewVaultItemFromOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.addNewVaultItem(); - } - - /** - * Redirects the overlay focus out of an overlay iframe. - * - * @param data - Contains the direction to redirect the focus. - */ - private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); - } - - /** - * Updates whether the current tab has ciphers that can populate the overlay list - * - * @param data - Contains the isOverlayCiphersPopulated value - * - */ - private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( - data?.isOverlayCiphersPopulated, - ); - } - - /** - * Updates the autofill overlay visibility. - * - * @param data - Contains the autoFillOverlayVisibility value - */ - private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { - return; - } - - this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; - } - - /** - * Clears the send collect details message timeout. - */ - private clearCollectPageDetailsOnLoadTimeout() { - if (this.collectPageDetailsOnLoadTimeout) { - clearTimeout(this.collectPageDetailsOnLoadTimeout); - } - } - - /** - * Sets up the extension message listeners for the content script. - */ - private setupExtensionMessageListeners() { - chrome.runtime.onMessage.addListener(this.handleExtensionMessage); - } - - /** - * Handles the extension messages sent to the content script. - * - * @param message - The extension message. - * @param sender - The message sender. - * @param sendResponse - The send response callback. - */ - private handleExtensionMessage = ( - message: AutofillExtensionMessage, - sender: chrome.runtime.MessageSender, - sendResponse: (response?: any) => void, - ): boolean => { - const command: string = message.command; - const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; - if (!handler) { - return null; - } - - const messageResponse = handler({ message, sender }); - if (typeof messageResponse === "undefined") { - return null; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; - }; - - /** - * Handles destroying the autofill init content script. Removes all - * listeners, timeouts, and object instances to prevent memory leaks. - */ - destroy() { - this.clearCollectPageDetailsOnLoadTimeout(); - chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); - this.collectAutofillContentService.destroy(); - this.autofillOverlayContentService?.destroy(); - } -} - -export default LegacyAutofillInit; diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts deleted file mode 100644 index 66d672172ae..00000000000 --- a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { setupAutofillInitDisconnectAction } from "../../utils"; -import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated"; - -import LegacyAutofillInit from "./autofill-init.deprecated"; - -(function (windowContext) { - if (!windowContext.bitwardenAutofillInit) { - const autofillOverlayContentService = new LegacyAutofillOverlayContentService(); - windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService); - setupAutofillInitDisconnectAction(windowContext); - - windowContext.bitwardenAutofillInit.init(); - } -})(window); diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts deleted file mode 100644 index b6b22be9439..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -type OverlayButtonMessage = { command: string; colorScheme?: string }; - -type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: AuthenticationStatus }; - -type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & { - styleSheetUrl: string; - translations: Record; -}; - -type OverlayButtonWindowMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayButton: ({ message }: { message: InitAutofillOverlayButtonMessage }) => void; - checkAutofillOverlayButtonFocused: () => void; - updateAutofillOverlayButtonAuthStatus: ({ - message, - }: { - message: UpdateAuthStatusMessage; - }) => void; - updateOverlayPageColorScheme: ({ message }: { message: OverlayButtonMessage }) => void; -}; - -export { - UpdateAuthStatusMessage, - OverlayButtonMessage, - InitAutofillOverlayButtonMessage, - OverlayButtonWindowMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts deleted file mode 100644 index 0c4160a0709..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts +++ /dev/null @@ -1,33 +0,0 @@ -type AutofillOverlayIframeExtensionMessage = { - command: string; - styles?: Partial; - theme?: string; -}; - -type AutofillOverlayIframeWindowMessageHandlers = { - [key: string]: CallableFunction; - updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void; - getPageColorScheme: () => void; -}; - -type AutofillOverlayIframeExtensionMessageParam = { - message: AutofillOverlayIframeExtensionMessage; -}; - -type BackgroundPortMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; - updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; - updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; -}; - -interface AutofillOverlayIframeService { - initOverlayIframe(initStyles: Partial, ariaAlert?: string): void; -} - -export { - AutofillOverlayIframeExtensionMessage, - AutofillOverlayIframeWindowMessageHandlers, - BackgroundPortMessageHandlers, - AutofillOverlayIframeService, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts deleted file mode 100644 index 83578b13043..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated"; - -type OverlayListMessage = { command: string }; - -type UpdateOverlayListCiphersMessage = OverlayListMessage & { - ciphers: OverlayCipherData[]; -}; - -type InitAutofillOverlayListMessage = OverlayListMessage & { - authStatus: AuthenticationStatus; - styleSheetUrl: string; - theme: string; - translations: Record; - ciphers?: OverlayCipherData[]; -}; - -type OverlayListWindowMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayList: ({ message }: { message: InitAutofillOverlayListMessage }) => void; - checkAutofillOverlayListFocused: () => void; - updateOverlayListCiphers: ({ message }: { message: UpdateOverlayListCiphersMessage }) => void; - focusOverlayList: () => void; -}; - -export { - UpdateOverlayListCiphersMessage, - InitAutofillOverlayListMessage, - OverlayListWindowMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts deleted file mode 100644 index 368ae4e7303..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated"; -import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated"; - -type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers; - -type AutofillOverlayPageElementWindowMessage = { - [key: string]: any; - command: string; - overlayCipherId?: string; - height?: number; -}; - -export { WindowMessageHandlers, AutofillOverlayPageElementWindowMessage }; diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap deleted file mode 100644 index 132bd968899..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AutofillOverlayIframeService initOverlayIframe creates an aria alert element if the ariaAlert param is passed 1`] = ` -
- aria alert -
-`; - -exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's attributes 1`] = ` -