1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 21:20:27 +00:00

Merge branch 'main' into auth/pm-19555/defect-clicking-log-out-button

This commit is contained in:
Alec Rippberger
2025-04-14 09:27:41 -05:00
committed by GitHub
37 changed files with 961 additions and 167 deletions

4
.github/CODEOWNERS vendored
View File

@@ -83,8 +83,6 @@ libs/common/src/platform @bitwarden/team-platform-dev
libs/common/spec @bitwarden/team-platform-dev
libs/common/src/state-migrations @bitwarden/team-platform-dev
libs/platform @bitwarden/team-platform-dev
# Node-specifc platform files
libs/node @bitwarden/team-key-management-dev
# Web utils used across app and connectors
apps/web/src/utils/ @bitwarden/team-platform-dev
# Web core and shared files
@@ -146,6 +144,8 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev
libs/key-management @bitwarden/team-key-management-dev
libs/key-management-ui @bitwarden/team-key-management-dev
libs/common/src/key-management @bitwarden/team-key-management-dev
# Node-cryptofunction service
libs/node @bitwarden/team-key-management-dev
apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev
apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev

View File

@@ -1098,6 +1098,31 @@
"message": "Login updated",
"description": "Message displayed when login details are successfully updated."
},
"loginUpdateTaskSuccess": {
"message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.",
"placeholders": {
"organization": {
"content": "$1"
}
},
"description": "Shown to user after login is updated."
},
"loginUpdateTaskSuccessAdditional": {
"message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.",
"placeholders": {
"organization": {
"content": "$1"
},
"task_count": {
"content": "$2"
}
},
"description": "Shown to user after login is updated."
},
"nextSecurityTaskAction": {
"message": "Change next password",
"description": "Message prompting user to undertake completion of another security task."
},
"saveFailure": {
"message": "Error saving",
"description": "Error message shown when the system fails to save login details."

View File

@@ -1,5 +1,5 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { html, TemplateResult } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
@@ -11,7 +11,7 @@ export function ActionButton({
theme,
handleClick,
}: {
buttonText: string;
buttonText: string | TemplateResult;
disabled?: boolean;
theme: Theme;
handleClick: (e: Event) => void;
@@ -63,4 +63,9 @@ const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: The
color: ${themes[theme].text.contrast};
}
`}
svg {
width: fit-content;
height: 16px;
}
`;

View File

@@ -0,0 +1,22 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function ExternalLink({ color, disabled, theme }: IconProps) {
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M1.5 2.75c0-.69.56-1.25 1.25-1.25h3.5a.75.75 0 0 0 0-1.5h-3.5A2.75 2.75 0 0 0 0 2.75v8.5A2.75 2.75 0 0 0 2.75 14h8.5A2.75 2.75 0 0 0 14 11.25v-3.5a.75.75 0 0 0-1.5 0v3.5c0 .69-.56 1.25-1.25 1.25h-8.5c-.69 0-1.25-.56-1.25-1.25v-8.5Z"
/>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M9.75 0a.75.75 0 0 0 0 1.5h1.69L7.22 5.72a.75.75 0 0 0 1.06 1.06l4.22-4.22v1.69a.75.75 0 0 0 1.5 0V.75a.75.75 0 0 0-.75-.75h-3.5Z"
/>
</svg>
`;
}

View File

@@ -4,9 +4,11 @@ export { BrandIconContainer } from "./brand-icon-container";
export { Business } from "./business";
export { Close } from "./close";
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";

View File

@@ -0,0 +1,222 @@
import { html } from "lit";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { IconProps } from "../common-types";
// This icon has static multi-colors for each theme
export function Keyhole({ theme }: IconProps) {
if (theme === ThemeTypes.Dark) {
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
<path
fill="#79A1E9"
fill-rule="evenodd"
d="M64.985 64.083c-.363-1.654.37-3.337 1.72-4.36C74.175 54.063 79 45.095 79 35 79 17.88 65.12 4 48 4 30.88 4 17 17.88 17 35c0 10.095 4.825 19.063 12.295 24.723 1.35 1.023 2.083 2.706 1.72 4.36l-5.947 27.058A4 4 0 0 0 28.975 96h38.05a4 4 0 0 0 3.907-4.859l-5.947-27.058Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M65.497 58.13C72.489 52.83 77 44.441 77 35 77 18.984 64.016 6 48 6S19 18.984 19 35c0 9.443 4.51 17.83 11.503 23.13 1.888 1.43 3.017 3.869 2.465 6.383L27.021 91.57A2 2 0 0 0 28.975 94h38.05a2 2 0 0 0 1.954-2.43l-5.947-27.057c-.552-2.514.577-4.954 2.465-6.384Zm1.208 1.593c-1.35 1.023-2.083 2.706-1.72 4.36l5.947 27.058A4 4 0 0 1 67.025 96h-38.05a4 4 0 0 1-3.907-4.859l5.947-27.058c.363-1.654-.37-3.337-1.72-4.36C21.825 54.063 17 45.095 17 35 17 17.88 30.88 4 48 4c17.12 0 31 13.88 31 31 0 10.095-4.825 19.063-12.295 24.723Z"
clip-rule="evenodd"
/>
<path
fill="#AAC3EF"
fill-rule="evenodd"
d="M20.864 50H46a4 4 0 0 0 4-4V30a4 4 0 0 0-4-4H20c-.647 0-1.258.154-1.8.427C17.42 29.149 17 32.026 17 35a30.86 30.86 0 0 0 3.864 15Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M22.07 48H46a2 2 0 0 0 2-2V30a2 2 0 0 0-2-2H20c-.051 0-.102.002-.151.006A29.072 29.072 0 0 0 19 35c0 4.678 1.106 9.092 3.07 13Zm-1.206 2H46a4 4 0 0 0 4-4V30a4 4 0 0 0-4-4H20c-.647 0-1.258.154-1.8.427C17.42 29.149 17 32.026 17 35a30.86 30.86 0 0 0 3.864 15Z"
clip-rule="evenodd"
/>
<path
fill="#79A1E9"
d="M18 31h30v4H18v-4ZM21 44a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1ZM29 44a1 1 0 0 1 1-1h9a1 1 0 1 1 0 2h-9a1 1 0 0 1-1-1Z"
/>
<path
fill="#AAC3EF"
fill-rule="evenodd"
d="M78.954 33.303A3.99 3.99 0 0 0 76 32H50a4 4 0 0 0-4 4v16a4 4 0 0 0 4 4h20.804A30.889 30.889 0 0 0 79 35a31.5 31.5 0 0 0-.046-1.697Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M76.99 34.262A1.987 1.987 0 0 0 76 34H50a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h19.91a28.88 28.88 0 0 0 7.08-19.738Zm1.964-.959A3.99 3.99 0 0 0 76 32H50a4 4 0 0 0-4 4v16a4 4 0 0 0 4 4h20.804A30.889 30.889 0 0 0 79 35a31.5 31.5 0 0 0-.046-1.697Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
d="M66 39a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1ZM66 43a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1Z"
/>
<path fill="#F3F6F9" d="M62 40a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" />
<path
fill="#175DDC"
fill-rule="evenodd"
d="M58 42a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm0 2a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
clip-rule="evenodd"
/>
<path
fill="#F3F6F9"
d="M65.391 48.269c.402 1.035.609 1.86.609 2.981a.75.75 0 0 1-.75.75h-14.5a.75.75 0 0 1-.75-.75c0-1.12.207-1.946.609-2.981a8.593 8.593 0 0 1 1.734-2.77 7.987 7.987 0 0 1 2.595-1.85C55.91 43.222 56.95 43 58 43c1.05 0 2.09.22 3.062.65a7.987 7.987 0 0 1 2.595 1.85 8.593 8.593 0 0 1 1.734 2.769Z"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M52.343 45.5a8.593 8.593 0 0 0-1.734 2.769c-.402 1.035-.609 1.86-.609 2.981 0 .414.336.75.75.75h14.5a.75.75 0 0 0 .75-.75c0-1.12-.207-1.946-.609-2.981a8.593 8.593 0 0 0-1.734-2.77 7.987 7.987 0 0 0-2.595-1.85A7.569 7.569 0 0 0 58 43c-1.05 0-2.09.22-3.062.65a7.987 7.987 0 0 0-2.595 1.85ZM63.857 50a8.12 8.12 0 0 0-.33-1.008 6.593 6.593 0 0 0-1.33-2.124l1.414-1.326-1.413 1.325a5.986 5.986 0 0 0-1.945-1.388A5.568 5.568 0 0 0 58 45c-.77 0-1.535.161-2.253.479a5.986 5.986 0 0 0-1.945 1.389 6.593 6.593 0 0 0-1.329 2.124 8.12 8.12 0 0 0-.33 1.008h11.714Z"
clip-rule="evenodd"
/>
<path
fill="#AAC3EF"
fill-rule="evenodd"
d="M67.604 76H48a8 8 0 0 0 0 16h23.027c0-.282-.031-.57-.095-.859L67.604 76Zm-38.769-2H50a8 8 0 1 0 0-16H27.215a31.14 31.14 0 0 0 2.08 1.723c1.35 1.023 2.083 2.706 1.72 4.36L28.835 74Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M29.628 60c1.134 1.034 1.719 2.571 1.387 4.083L28.835 74H50a8 8 0 1 0 0-16H27.215a31.14 31.14 0 0 0 2.08 1.723c.116.088.227.18.333.277Zm2.562 0c.792 1.307 1.134 2.894.778 4.513L31.322 72H50a6 6 0 1 0 0-12H32.19Zm33.806 18H48a6 6 0 0 0 0 12h20.633l-2.637-12Zm1.608-2H48a8 8 0 0 0 0 16h23.027c0-.282-.031-.57-.095-.859L67.604 76Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="m69.06 82.627-1.056.346v-1.4a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.832 1.167a1 1 0 0 0 1.627 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167.883-.29-.431-1.962ZM29.819 69.526a.996.996 0 0 0 .376-.318l.81-1.135.81 1.135a1 1 0 1 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 1 0-.623-1.901l-1.312.43v-1.4a1 1 0 0 0-.95-.999c.08.493.072 1.002-.04 1.51l-1.195 5.442Zm11.186-5.953a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.832 1.167a1 1 0 0 0 1.627 1.162l.81-1.135.81 1.135a1 1 0 1 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 1 0-.623-1.901l-1.312.43v-1.4Zm9 0a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.832 1.167a1 1 0 0 0 1.627 1.162l.81-1.135.81 1.135a1 1 0 1 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 1 0-.623-1.901l-1.312.43v-1.4Zm0 18a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.832 1.167a1 1 0 0 0 1.627 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 1 0-.623-1.901l-1.312.43v-1.4Zm9 0a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.832 1.167a1 1 0 0 0 1.627 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 1 0-.623-1.901l-1.312.43v-1.4Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M65.497 58.13C72.489 52.83 77 44.441 77 35 77 18.984 64.016 6 48 6S19 18.984 19 35c0 9.443 4.51 17.83 11.503 23.13 1.888 1.43 3.017 3.869 2.465 6.383L27.021 91.57A2 2 0 0 0 28.975 94h38.05a2 2 0 0 0 1.954-2.43l-5.947-27.057c-.552-2.514.577-4.954 2.465-6.384Zm1.208 1.593c-1.35 1.023-2.083 2.706-1.72 4.36l5.947 27.058A4 4 0 0 1 67.025 96h-38.05a4 4 0 0 1-3.907-4.859l5.947-27.058c.363-1.654-.37-3.337-1.72-4.36C21.825 54.063 17 45.095 17 35 17 17.88 30.88 4 48 4c17.12 0 31 13.88 31 31 0 10.095-4.825 19.063-12.295 24.723Z"
clip-rule="evenodd"
/>
<path
fill="#FFBF00"
d="M60 12c0 6.627-5.373 12-12 12s-12-5.373-12-12S41.373 0 48 0s12 5.373 12 12Z"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M48 22c5.523 0 10-4.477 10-10S53.523 2 48 2 38 6.477 38 12s4.477 10 10 10Zm0 2c6.627 0 12-5.373 12-12S54.627 0 48 0 36 5.373 36 12s5.373 12 12 12Z"
clip-rule="evenodd"
/>
<path
fill="#175DDC"
fill-rule="evenodd"
d="M53.707 8.293a1 1 0 0 1 0 1.414l-7 7a1 1 0 0 1-1.414 0l-3-3a1 1 0 0 1 1.414-1.414L46 14.586l6.293-6.293a1 1 0 0 1 1.414 0Z"
clip-rule="evenodd"
/>
</svg>
`;
}
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
<path
fill="#99BAF4"
fill-rule="evenodd"
d="M64.985 64.083c-.363-1.654.37-3.337 1.72-4.36C74.175 54.063 79 45.095 79 35 79 17.88 65.12 4 48 4 30.88 4 17 17.88 17 35c0 10.095 4.825 19.063 12.295 24.723 1.35 1.023 2.083 2.706 1.72 4.36l-5.947 27.058A4 4 0 0 0 28.975 96h38.05a4 4 0 0 0 3.907-4.859l-5.947-27.058Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M65.497 58.13C72.489 52.83 77 44.441 77 35 77 18.984 64.016 6 48 6S19 18.984 19 35c0 9.443 4.51 17.83 11.503 23.13 1.888 1.43 3.017 3.869 2.465 6.383L27.021 91.57A2 2 0 0 0 28.975 94h38.05a2 2 0 0 0 1.954-2.43l-5.947-27.057c-.552-2.514.577-4.954 2.465-6.384Zm1.208 1.593c-1.35 1.023-2.083 2.706-1.72 4.36l5.947 27.058A4 4 0 0 1 67.025 96h-38.05a4 4 0 0 1-3.907-4.859l5.947-27.058c.363-1.654-.37-3.337-1.72-4.36C21.825 54.063 17 45.095 17 35 17 17.88 30.88 4 48 4c17.12 0 31 13.88 31 31 0 10.095-4.825 19.063-12.295 24.723Z"
clip-rule="evenodd"
/>
<path
fill="#DBE5F6"
fill-rule="evenodd"
d="M20.864 50H46a4 4 0 0 0 4-4V30a4 4 0 0 0-4-4H20c-.647 0-1.258.154-1.8.427C17.42 29.149 17 32.026 17 35a30.86 30.86 0 0 0 3.864 15Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M22.07 48H46a2 2 0 0 0 2-2V30a2 2 0 0 0-2-2H20c-.051 0-.102.002-.151.006A29.072 29.072 0 0 0 19 35c0 4.678 1.106 9.092 3.07 13Zm-1.206 2H46a4 4 0 0 0 4-4V30a4 4 0 0 0-4-4H20c-.647 0-1.258.154-1.8.427C17.42 29.149 17 32.026 17 35a30.86 30.86 0 0 0 3.864 15Z"
clip-rule="evenodd"
/>
<path
fill="#99BAF4"
d="M18 31h30v4H18v-4ZM21 44a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1ZM29 44a1 1 0 0 1 1-1h9a1 1 0 1 1 0 2h-9a1 1 0 0 1-1-1Z"
/>
<path
fill="#DBE5F6"
fill-rule="evenodd"
d="M78.954 33.303A3.99 3.99 0 0 0 76 32H50a4 4 0 0 0-4 4v16a4 4 0 0 0 4 4h20.804A30.889 30.889 0 0 0 79 35a31.5 31.5 0 0 0-.046-1.697Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M76.99 34.262A1.987 1.987 0 0 0 76 34H50a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h19.91a28.88 28.88 0 0 0 7.08-19.738Zm1.964-.959A3.99 3.99 0 0 0 76 32H50a4 4 0 0 0-4 4v16a4 4 0 0 0 4 4h20.804A30.889 30.889 0 0 0 79 35a31.5 31.5 0 0 0-.046-1.697Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
d="M66 39a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1ZM66 43a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1Z"
/>
<path fill="#fff" d="M62 40a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" />
<path
fill="#0E3781"
fill-rule="evenodd"
d="M58 42a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm0 2a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
clip-rule="evenodd"
/>
<path
fill="#fff"
d="M65.391 48.269c.402 1.035.609 1.86.609 2.981a.75.75 0 0 1-.75.75h-14.5a.75.75 0 0 1-.75-.75c0-1.12.207-1.946.609-2.981a8.593 8.593 0 0 1 1.734-2.77 7.987 7.987 0 0 1 2.595-1.85C55.91 43.222 56.95 43 58 43c1.05 0 2.09.22 3.062.65a7.987 7.987 0 0 1 2.595 1.85 8.593 8.593 0 0 1 1.734 2.769Z"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M52.343 45.5a8.593 8.593 0 0 0-1.734 2.769c-.402 1.035-.609 1.86-.609 2.981 0 .414.336.75.75.75h14.5a.75.75 0 0 0 .75-.75c0-1.12-.207-1.946-.609-2.981a8.593 8.593 0 0 0-1.734-2.77 7.987 7.987 0 0 0-2.595-1.85A7.569 7.569 0 0 0 58 43c-1.05 0-2.09.22-3.062.65a7.987 7.987 0 0 0-2.595 1.85ZM63.857 50a8.12 8.12 0 0 0-.33-1.008 6.593 6.593 0 0 0-1.33-2.124l1.414-1.326-1.413 1.325a5.986 5.986 0 0 0-1.945-1.388A5.568 5.568 0 0 0 58 45c-.77 0-1.535.161-2.253.479a5.986 5.986 0 0 0-1.945 1.389 6.593 6.593 0 0 0-1.329 2.124 8.12 8.12 0 0 0-.33 1.008h11.714Z"
clip-rule="evenodd"
/>
<path
fill="#DBE5F6"
fill-rule="evenodd"
d="M67.605 76H48a8 8 0 1 0 0 16h23.027c0-.282-.031-.57-.095-.859L67.604 76Zm-38.77-2H50a8 8 0 0 0 0-16H27.215a31.14 31.14 0 0 0 2.08 1.723c1.35 1.023 2.083 2.706 1.72 4.36L28.835 74Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M29.628 60c1.134 1.034 1.72 2.571 1.387 4.083L28.835 74H50a8 8 0 0 0 0-16H27.215a31.117 31.117 0 0 0 2.08 1.723c.116.088.227.18.333.277Zm2.562 0c.792 1.307 1.134 2.894.778 4.513L31.322 72H50a6 6 0 0 0 0-12H32.19Zm33.806 18H48a6 6 0 0 0 0 12h20.634l-2.638-12Zm1.609-2H48a8 8 0 1 0 0 16h23.027c0-.282-.031-.57-.095-.859L67.604 76Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="m69.06 82.627-1.056.346v-1.4a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.832 1.167a1 1 0 1 0 1.627 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167.883-.29-.431-1.962ZM29.819 69.526a.995.995 0 0 0 .376-.318l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 0 0-.623-1.901l-1.312.43v-1.4a1 1 0 0 0-.95-.999c.08.493.072 1.002-.04 1.51l-1.196 5.442Zm11.186-5.953a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.833 1.167a1 1 0 1 0 1.628 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 0 0-.623-1.901l-1.312.43v-1.4Zm9 0a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.833 1.167a1 1 0 1 0 1.628 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 0 0-.623-1.901l-1.312.43v-1.4Zm0 18a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.833 1.167a1 1 0 1 0 1.628 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 0 0-.623-1.901l-1.312.43v-1.4Zm9 0a1 1 0 1 0-2 0v1.4l-1.312-.43a1 1 0 0 0-.623 1.9l1.33.436-.833 1.167a1 1 0 1 0 1.628 1.162l.81-1.135.81 1.135a1 1 0 0 0 1.628-1.162l-.833-1.167 1.33-.435a1 1 0 0 0-.623-1.901l-1.312.43v-1.4Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M65.497 58.13C72.489 52.83 77 44.441 77 35 77 18.984 64.016 6 48 6S19 18.984 19 35c0 9.443 4.51 17.83 11.503 23.13 1.888 1.43 3.017 3.869 2.465 6.383L27.021 91.57A2 2 0 0 0 28.975 94h38.05a2 2 0 0 0 1.954-2.43l-5.947-27.057c-.552-2.514.577-4.954 2.465-6.384Zm1.208 1.593c-1.35 1.023-2.083 2.706-1.72 4.36l5.947 27.058A4 4 0 0 1 67.025 96h-38.05a4 4 0 0 1-3.907-4.859l5.947-27.058c.363-1.654-.37-3.337-1.72-4.36C21.825 54.063 17 45.095 17 35 17 17.88 30.88 4 48 4c17.12 0 31 13.88 31 31 0 10.095-4.825 19.063-12.295 24.723Z"
clip-rule="evenodd"
/>
<path
fill="#FFBF00"
d="M60 12c0 6.627-5.373 12-12 12s-12-5.373-12-12S41.373 0 48 0s12 5.373 12 12Z"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M48 22c5.523 0 10-4.477 10-10S53.523 2 48 2 38 6.477 38 12s4.477 10 10 10Zm0 2c6.627 0 12-5.373 12-12S54.627 0 48 0 36 5.373 36 12s5.373 12 12 12Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M53.707 8.293a1 1 0 0 1 0 1.414l-7 7a1 1 0 0 1-1.414 0l-3-3a1 1 0 0 1 1.414-1.414L46 14.586l6.293-6.293a1 1 0 0 1 1.414 0Z"
clip-rule="evenodd"
/>
</svg>
`;
}

View File

@@ -57,9 +57,11 @@ export const BusinessIcon = createIconStory("Business");
export const BrandIcon = createIconStory("BrandIconContainer");
export const CloseIcon = createIconStory("Close");
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");

View File

@@ -16,7 +16,7 @@ type Args = {
};
export default {
title: "Components/Notifications/Notification Body",
title: "Components/Notifications/Body",
argTypes: {
ciphers: { control: "object" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },

View File

@@ -1,29 +1,26 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationConfirmationBody } from "../../notification/confirmation";
type Args = {
buttonText: string;
confirmationMessage: string;
handleOpenVault: () => void;
theme: Theme;
error: string;
};
import {
NotificationConfirmationBody,
NotificationConfirmationBodyProps,
} from "../../../notification/confirmation/body";
export default {
title: "Components/Notifications/Notification Confirmation Body",
title: "Components/Notifications/Confirmation/Body",
argTypes: {
error: { control: "text" },
buttonText: { control: "text" },
confirmationMessage: { control: "text" },
messageDetails: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
error: "",
buttonText: "View",
confirmationMessage: "[item name] updated in Bitwarden.",
messageDetails: "You can view it in your vault.",
theme: ThemeTypes.Light,
},
parameters: {
@@ -32,10 +29,11 @@ export default {
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev",
},
},
} as Meta<Args>;
} as Meta<NotificationConfirmationBodyProps>;
const Template = (args: Args) => NotificationConfirmationBody({ ...args });
const Template = (args: NotificationConfirmationBodyProps) =>
NotificationConfirmationBody({ ...args });
export const Default: StoryObj<Args> = {
export const Default: StoryObj<NotificationConfirmationBodyProps> = {
render: Template,
};

View File

@@ -0,0 +1,55 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
import {
NotificationConfirmationContainer,
NotificationConfirmationContainerProps,
} from "../../../notification/confirmation/container";
export default {
title: "Components/Notifications/Confirmation",
argTypes: {
error: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
type: { control: "select", options: [NotificationTypes.Change, NotificationTypes.Add] },
},
args: {
error: "",
task: {
orgName: "Acme, Inc.",
remainingTasksCount: 0,
},
handleCloseNotification: () => alert("Close notification action triggered"),
handleOpenTasks: () => alert("Open tasks action triggered"),
i18n: {
loginSaveSuccess: "Login saved",
loginUpdateSuccess: "Login updated",
loginUpdateTaskSuccessAdditional:
"Thank you for making your organization more secure. You have 3 more passwords to update.",
loginUpdateTaskSuccess:
"Great job! You took the steps to make you and your organization more secure.",
nextSecurityTaskAction: "Change next password",
saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.",
view: "View",
},
type: NotificationTypes.Change,
username: "Acme, Inc. Login",
theme: ThemeTypes.Light,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev",
},
},
} as Meta<NotificationConfirmationContainerProps>;
const Template = (args: NotificationConfirmationContainerProps) =>
NotificationConfirmationContainer({ ...args });
export const Default: StoryObj<NotificationConfirmationContainerProps> = {
render: Template,
};

View File

@@ -0,0 +1,36 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import {
NotificationConfirmationFooter,
NotificationConfirmationFooterProps,
} from "../../../notification/confirmation/footer";
export default {
title: "Components/Notifications/Confirmation/Footer",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
handleButtonClick: () => alert("Action button triggered"),
i18n: {
nextSecurityTaskAction: "Change next password",
},
theme: ThemeTypes.Light,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev",
},
},
} as Meta<NotificationConfirmationFooterProps>;
const Template = (args: NotificationConfirmationFooterProps) =>
html`<div style="max-width:400px;">${NotificationConfirmationFooter({ ...args })}</div>`;
export const Default: StoryObj<NotificationConfirmationFooterProps> = {
render: Template,
};

View File

@@ -0,0 +1,37 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import {
NotificationConfirmationMessage,
NotificationConfirmationMessageProps,
} from "../../../notification/confirmation/message";
export default {
title: "Components/Notifications/Confirmation/Message",
argTypes: {
buttonText: { control: "text" },
message: { control: "text" },
messageDetails: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
buttonText: "View",
message: "[item name] updated in Bitwarden.",
messageDetails: "It was added to your vault.",
theme: ThemeTypes.Light,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev",
},
},
} as Meta<NotificationConfirmationMessageProps>;
const Template = (args: NotificationConfirmationMessageProps) =>
NotificationConfirmationMessage({ ...args });
export const Default: StoryObj<NotificationConfirmationMessageProps> = {
render: Template,
};

View File

@@ -0,0 +1,61 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
import { NotificationContainer, NotificationContainerProps } from "../../notification/container";
export default {
title: "Components/Notifications",
argTypes: {
error: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
type: { control: "select", options: [...Object.values(NotificationTypes)] },
},
args: {
error: "",
ciphers: [
{
id: "1",
name: "Example Cipher",
type: CipherType.Login,
favorite: false,
reprompt: CipherRepromptType.None,
icon: {
imageEnabled: true,
image: "",
fallbackImage: "https://example.com/fallback.png",
icon: "icon-class",
},
login: { username: "user@example.com" },
},
],
i18n: {
loginSaveSuccess: "Login saved",
loginUpdateSuccess: "Login updated",
saveAction: "Save",
saveAsNewLoginAction: "Save as new login",
saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.",
updateLoginPrompt: "Update existing login?",
view: "View",
},
type: NotificationTypes.Change,
username: "mockUsername",
theme: ThemeTypes.Light,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev",
},
},
} as Meta<NotificationContainerProps>;
const Template = (args: NotificationContainerProps) => NotificationContainer({ ...args });
export const Default: StoryObj<NotificationContainerProps> = {
render: Template,
};

View File

@@ -7,7 +7,7 @@ import { NotificationFooter, NotificationFooterProps } from "../../notification/
import { mockFolderData, mockOrganizationData } from "../mock-data";
export default {
title: "Components/Notifications/Notification Footer",
title: "Components/Notifications/Footer",
argTypes: {
notificationType: {
control: "select",

View File

@@ -12,7 +12,7 @@ type Args = {
};
export default {
title: "Components/Notifications/Notification Header",
title: "Components/Notifications/Header",
argTypes: {
message: { control: "text" },
standalone: { control: "boolean" },

View File

@@ -1,53 +0,0 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { themes } from "../constants/styles";
export function NotificationConfirmationMessage({
buttonText,
confirmationMessage,
handleClick,
theme,
}: {
buttonText: string;
confirmationMessage: string;
handleClick: (e: Event) => void;
theme: Theme;
}) {
return html`
<span title=${confirmationMessage} class=${notificationConfirmationMessageStyles(theme)}
>${confirmationMessage}
<a
title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick}
>${buttonText}</a
></span
>
`;
}
const baseTextStyles = css`
flex-grow: 1;
overflow-x: hidden;
text-align: left;
text-overflow: ellipsis;
line-height: 24px;
font-family: "DM Sans", sans-serif;
font-size: 16px;
`;
const notificationConfirmationMessageStyles = (theme: Theme) => css`
${baseTextStyles}
color: ${themes[theme].text.main};
font-weight: 400;
`;
const notificationConfirmationButtonTextStyles = (theme: Theme) => css`
${baseTextStyles}
color: ${themes[theme].primary[600]};
font-weight: 700;
cursor: pointer;
`;

View File

@@ -1,12 +1,12 @@
import createEmotion from "@emotion/css/create-instance";
import { html } from "lit";
import { html, nothing } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { themes } from "../constants/styles";
import { PartyHorn, Warning } from "../icons";
import { themes } from "../../constants/styles";
import { PartyHorn, Keyhole, Warning } from "../../icons";
import { NotificationConfirmationMessage } from "./confirmation-message";
import { NotificationConfirmationMessage } from "./message";
export const componentClassPrefix = "notification-confirmation-body";
@@ -14,31 +14,41 @@ const { css } = createEmotion({
key: componentClassPrefix,
});
export function NotificationConfirmationBody({
buttonText,
error,
confirmationMessage,
theme,
handleOpenVault,
}: {
error?: string;
export type NotificationConfirmationBodyProps = {
buttonText: string;
confirmationMessage: string;
error?: string;
messageDetails?: string;
tasksAreComplete?: boolean;
theme: Theme;
handleOpenVault: (e: Event) => void;
}) {
const IconComponent = !error ? PartyHorn : Warning;
};
export function NotificationConfirmationBody({
buttonText,
confirmationMessage,
error,
messageDetails,
tasksAreComplete,
theme,
handleOpenVault,
}: NotificationConfirmationBodyProps) {
const IconComponent = tasksAreComplete ? Keyhole : !error ? PartyHorn : Warning;
const showConfirmationMessage = confirmationMessage || buttonText || messageDetails;
return html`
<div class=${notificationConfirmationBodyStyles({ theme })}>
<div class=${iconContainerStyles(error)}>${IconComponent({ theme })}</div>
${confirmationMessage && buttonText
${showConfirmationMessage
? NotificationConfirmationMessage({
handleClick: handleOpenVault,
confirmationMessage,
theme,
buttonText,
message: confirmationMessage,
messageDetails,
theme,
handleClick: handleOpenVault,
})
: null}
: nothing}
</div>
`;
}

View File

@@ -1,42 +1,67 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { html, nothing } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import {
NotificationBarIframeInitData,
NotificationTypes,
NotificationTaskInfo,
NotificationType,
} from "../../../notification/abstractions/notification-bar";
import { themes, spacing } from "../constants/styles";
import { NotificationConfirmationBody } from "./confirmation";
NotificationTypes,
} from "../../../../notification/abstractions/notification-bar";
import { themes, spacing } from "../../constants/styles";
import {
NotificationHeader,
componentClassPrefix as notificationHeaderClassPrefix,
} from "./header";
} from "../header";
import { NotificationConfirmationBody } from "./body";
import { NotificationConfirmationFooter } from "./footer";
export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
handleOpenVault: (e: Event) => void;
handleOpenTasks: (e: Event) => void;
} & {
error?: string;
i18n: { [key: string]: string };
task?: NotificationTaskInfo;
type: NotificationType;
username: string;
};
export function NotificationConfirmationContainer({
error,
handleCloseNotification,
handleOpenVault,
handleOpenTasks,
i18n,
task,
theme = ThemeTypes.Light,
type,
username,
}: NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
handleOpenVault: (e: Event) => void;
} & {
error?: string;
i18n: { [key: string]: string };
type: NotificationType;
username: string;
}) {
}: NotificationConfirmationContainerProps) {
const headerMessage = getHeaderMessage(i18n, type, error);
const confirmationMessage = getConfirmationMessage(i18n, username, type, error);
const buttonText = error ? i18n.newItem : i18n.view;
let messageDetails: string | undefined;
let remainingTasksCount: number | undefined;
let tasksAreComplete: boolean = false;
if (task) {
remainingTasksCount = task.remainingTasksCount || 0;
tasksAreComplete = remainingTasksCount === 0;
messageDetails =
remainingTasksCount > 0
? chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional", [
task.orgName,
`${remainingTasksCount}`,
])
: chrome.i18n.getMessage("loginUpdateTaskSuccess", [task.orgName]);
}
return html`
<div class=${notificationContainerStyles(theme)}>
${NotificationHeader({
@@ -47,10 +72,18 @@ export function NotificationConfirmationContainer({
${NotificationConfirmationBody({
buttonText,
confirmationMessage,
error: error,
handleOpenVault,
tasksAreComplete,
messageDetails,
theme,
handleOpenVault,
})}
${remainingTasksCount
? NotificationConfirmationFooter({
i18n,
theme,
handleButtonClick: handleOpenTasks,
})
: nothing}
</div>
`;
}

View File

@@ -0,0 +1,59 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { ActionButton } from "../../buttons/action-button";
import { spacing, themes } from "../../constants/styles";
import { ExternalLink } from "../../icons";
export type NotificationConfirmationFooterProps = {
i18n: { [key: string]: string };
theme: Theme;
handleButtonClick: (event: Event) => void;
};
export function NotificationConfirmationFooter({
i18n,
theme,
handleButtonClick,
}: NotificationConfirmationFooterProps) {
const primaryButtonText = i18n.nextSecurityTaskAction;
return html`
<div class=${notificationConfirmationFooterStyles({ theme })}>
${ActionButton({
handleClick: handleButtonClick,
buttonText: AdditionalTasksButtonContent({ buttonText: primaryButtonText, theme }),
theme,
})}
</div>
`;
}
const notificationConfirmationFooterStyles = ({ theme }: { theme: Theme }) => css`
background-color: ${themes[theme].background.alt};
padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]};
max-width: min-content;
:last-child {
border-radius: 0 0 ${spacing["4"]} ${spacing["4"]};
padding-bottom: ${spacing[4]};
}
`;
function AdditionalTasksButtonContent({ buttonText, theme }: { buttonText: string; theme: Theme }) {
return html`
<div class=${additionalTasksButtonContentStyles({ theme })}>
<span>${buttonText}</span>
${ExternalLink({ theme, color: themes[theme].text.contrast })}
</div>
`;
}
const additionalTasksButtonContentStyles = ({ theme }: { theme: Theme }) => css`
gap: ${spacing[2]};
display: flex;
align-items: center;
white-space: nowrap;
`;

View File

@@ -0,0 +1,83 @@
import { css } from "@emotion/css";
import { html, nothing } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { themes, typography } from "../../constants/styles";
export type NotificationConfirmationMessageProps = {
buttonText?: string;
message?: string;
messageDetails?: string;
handleClick: (e: Event) => void;
theme: Theme;
};
export function NotificationConfirmationMessage({
buttonText,
message,
messageDetails,
handleClick,
theme,
}: NotificationConfirmationMessageProps) {
return html`
<div>
${message || buttonText
? html`
<span
title=${message || buttonText}
class=${notificationConfirmationMessageStyles(theme)}
>
${message || nothing}
${buttonText
? html`
<a
title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick}
>
${buttonText}
</a>
`
: nothing}
</span>
`
: nothing}
${messageDetails
? html`<div class=${AdditionalMessageStyles({ theme })}>${messageDetails}</div>`
: nothing}
</div>
`;
}
const baseTextStyles = css`
flex-grow: 1;
overflow-x: hidden;
text-align: left;
text-overflow: ellipsis;
line-height: 24px;
font-family: "DM Sans", sans-serif;
font-size: 16px;
`;
const notificationConfirmationMessageStyles = (theme: Theme) => css`
${baseTextStyles}
color: ${themes[theme].text.main};
font-weight: 400;
`;
const notificationConfirmationButtonTextStyles = (theme: Theme) => css`
${baseTextStyles}
color: ${themes[theme].primary[600]};
font-weight: 700;
cursor: pointer;
`;
const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css`
${typography.body2}
font-size: 14px;
color: ${themes[theme].text.muted};
`;

View File

@@ -19,6 +19,18 @@ import {
componentClassPrefix as notificationHeaderClassPrefix,
} from "./header";
export type NotificationContainerProps = NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
handleSaveAction: (e: Event) => void;
handleEditOrUpdateAction: (e: Event) => void;
} & {
ciphers?: NotificationCipherData[];
folders?: FolderView[];
i18n: { [key: string]: string };
organizations?: OrgView[];
type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type`
};
export function NotificationContainer({
handleCloseNotification,
handleEditOrUpdateAction,
@@ -29,17 +41,7 @@ export function NotificationContainer({
organizations,
theme = ThemeTypes.Light,
type,
}: NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
handleSaveAction: (e: Event) => void;
handleEditOrUpdateAction: (e: Event) => void;
} & {
ciphers?: NotificationCipherData[];
folders?: FolderView[];
i18n: { [key: string]: string };
organizations?: OrgView[];
type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type`
}) {
}: NotificationContainerProps) {
const headerMessage = getHeaderMessage(i18n, type);
const showBody = true;

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { Message, MessageType } from "./message";
const SENDER = "bitwarden-webauthn";
@@ -126,17 +124,11 @@ export class Messenger {
}
};
let onDestroyListener;
const destroyPromise: Promise<never> = new Promise((_, reject) => {
onDestroyListener = () => reject(new FallbackRequestedError());
this.onDestroy.addEventListener("destroy", onDestroyListener);
});
const onDestroyListener = () => abortController.abort();
this.onDestroy.addEventListener("destroy", onDestroyListener);
try {
const handlerResponse = await Promise.race([
this.handler(message, abortController),
destroyPromise,
]);
const handlerResponse = await this.handler(message, abortController);
port.postMessage({ ...handlerResponse, SENDER });
} catch (error) {
port.postMessage({

View File

@@ -11,6 +11,11 @@ const NotificationTypes = {
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
type NotificationTaskInfo = {
orgName: string;
remainingTasksCount: number;
};
type NotificationBarIframeInitData = {
ciphers?: NotificationCipherData[];
folders?: FolderView[];
@@ -38,6 +43,7 @@ type NotificationBarWindowMessageHandlers = {
};
export {
NotificationTaskInfo,
NotificationTypes,
NotificationType,
NotificationBarIframeInitData,

View File

@@ -7,7 +7,7 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
import { NotificationCipherData } from "../content/components/cipher/types";
import { OrgView } from "../content/components/common-types";
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container";
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
import { NotificationContainer } from "../content/components/notification/container";
import { buildSvgDomElement } from "../utils";
import { circleCheckIcon } from "../utils/svg-icons";
@@ -58,6 +58,9 @@ function getI18n() {
loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"),
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"),
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
newItem: chrome.i18n.getMessage("newItem"),
never: chrome.i18n.getMessage("never"),
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
@@ -369,6 +372,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
error,
username: username ?? i18n.typeLogin,
handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId),
handleOpenTasks: () => {},
}),
document.body,
);

View File

@@ -261,6 +261,7 @@ import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.s
import { BrowserApi } from "../platform/browser/browser-api";
import { flagEnabled } from "../platform/flags";
import { IpcBackgroundService } from "../platform/ipc/ipc-background.service";
import { IpcContentScriptManagerService } from "../platform/ipc/ipc-content-script-manager.service";
import { UpdateBadge } from "../platform/listeners/update-badge";
/* eslint-disable no-restricted-imports */
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
@@ -405,6 +406,7 @@ export default class MainBackground {
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
taskService: TaskService;
ipcContentScriptManagerService: IpcContentScriptManagerService;
ipcService: IpcService;
onUpdatedRan: boolean;
@@ -1314,6 +1316,7 @@ export default class MainBackground {
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
this.ipcService = new IpcBackgroundService(this.logService);
}

View File

@@ -0,0 +1,42 @@
import { mergeMap } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BrowserApi } from "../browser/browser-api";
const IPC_CONTENT_SCRIPT_ID = "ipc-content-script";
export class IpcContentScriptManagerService {
constructor(configService: ConfigService) {
if (!BrowserApi.isManifestVersion(3)) {
// IPC not supported on MV2
return;
}
configService
.getFeatureFlag$(FeatureFlag.IpcChannelFramework)
.pipe(
mergeMap(async (enabled) => {
if (!enabled) {
return;
}
try {
await BrowserApi.unregisterContentScriptsMv3({ ids: [IPC_CONTENT_SCRIPT_ID] });
} catch {
// Ignore errors
}
await BrowserApi.registerContentScriptsMv3([
{
id: IPC_CONTENT_SCRIPT_ID,
matches: ["https://*/*"],
js: ["content/ipc-content-script.js"],
},
]);
}),
)
.subscribe();
}
}

View File

@@ -1 +0,0 @@
@import "../scss/styles.scss";

View File

@@ -0,0 +1,51 @@
import { redirectToDuoFrameless } from "./duo-redirect";
describe("duo-redirect", () => {
describe("redirectToDuoFrameless", () => {
beforeEach(() => {
Object.defineProperty(window, "location", {
value: { href: "" },
writable: true,
});
});
it("should redirect to a valid Duo URL", () => {
const validUrl = "https://api-123.duosecurity.com/auth";
redirectToDuoFrameless(validUrl);
expect(window.location.href).toBe(validUrl);
});
it("should redirect to a valid Duo Federal URL", () => {
const validUrl = "https://api-123.duofederal.com/auth";
redirectToDuoFrameless(validUrl);
expect(window.location.href).toBe(validUrl);
});
it("should throw an error for an invalid URL", () => {
const invalidUrl = "https://malicious-site.com";
expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL");
});
it("should throw an error for an malicious URL with valid redirect embedded", () => {
const invalidUrl = "https://malicious-site.com\\@api-123.duosecurity.com/auth";
expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL");
});
it("should throw an error for a non-HTTPS URL", () => {
const nonHttpsUrl = "http://api-123.duosecurity.com/auth";
expect(() => redirectToDuoFrameless(nonHttpsUrl)).toThrow("Invalid redirect URL");
});
it("should throw an error for a URL with an invalid hostname", () => {
const invalidHostnameUrl = "https://api-123.invalid.com";
expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL");
});
it("should throw an error for a URL with credentials", () => {
const UrlWithCredentials = "https://api-123.duosecurity.com:password@evil/attack";
expect(() => redirectToDuoFrameless(UrlWithCredentials)).toThrow(
"Invalid redirect URL: embedded credentials not allowed",
);
});
});
});

View File

@@ -1,14 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { getQsParam } from "./common";
import { TranslationService } from "./translation.service";
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./duo-redirect.scss");
const mobileDesktopCallback = "bitwarden://duo-callback";
let localeService: TranslationService = null;
let localeService: TranslationService | null = null;
window.addEventListener("load", async () => {
const redirectUrl = getQsParam("duoFramelessUrl");
@@ -18,9 +12,18 @@ window.addEventListener("load", async () => {
return;
}
const client = getQsParam("client");
const code = getQsParam("code");
const state = getQsParam("state");
const client: string | null = getQsParam("client");
const code: string | null = getQsParam("code");
const state: string | null = getQsParam("state");
if (!client) {
throw new Error("client is null");
}
if (!code) {
throw new Error("code is null");
}
if (!state) {
throw new Error("state is null");
}
localeService = new TranslationService(navigator.language, "locales");
await localeService.init();
@@ -53,16 +56,28 @@ window.addEventListener("load", async () => {
* validate the Duo AuthUrl and redirect to it.
* @param redirectUrl the duo auth url
*/
function redirectToDuoFrameless(redirectUrl: string) {
const validateUrl = new URL(redirectUrl);
const validDuoUrl =
validateUrl.protocol === "https:" &&
(validateUrl.hostname.endsWith(".duosecurity.com") ||
validateUrl.hostname.endsWith(".duofederal.com"));
if (!validDuoUrl) {
export function redirectToDuoFrameless(redirectUrl: string) {
// Regex to match a valid duo redirect URL
/**
* This regex checks for the following:
* The string must start with "https://api-"
* Followed by a subdomain that can contain letters, numbers
* Followed by either "duosecurity.com" or "duofederal.com"
* This ensures that the redirect does not contain any malicious content
* and is a valid Duo URL.
* */
const duoRedirectUrlRegex = /^https:\/\/api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com/;
// Check if the redirect URL matches the regex
if (!duoRedirectUrlRegex.test(redirectUrl)) {
throw new Error("Invalid redirect URL");
}
// At this point we know the URL to be valid, but we need to check for embedded credentials
const validateUrl = new URL(redirectUrl);
// URLs should not contain
// Check that no embedded credentials are present
if (validateUrl.username || validateUrl.password) {
throw new Error("Invalid redirect URL: embedded credentials not allowed");
}
window.location.href = decodeURIComponent(redirectUrl);
}
@@ -72,17 +87,23 @@ function redirectToDuoFrameless(redirectUrl: string) {
* so browser, desktop, and mobile are not able to take advantage of the countdown timer or close button.
*/
function displayHandoffMessage(client: string) {
const content = document.getElementById("content");
const content: HTMLElement | null = document.getElementById("content");
if (!content) {
throw new Error("content element not found");
}
content.className = "text-center";
content.innerHTML = "";
const h1 = document.createElement("h1");
const p = document.createElement("p");
const p: HTMLElement = document.createElement("p");
if (!localeService) {
throw new Error("localeService is not initialized");
}
h1.textContent = localeService.t("youSuccessfullyLoggedIn");
p.textContent =
client == "web"
? (p.textContent = localeService.t("thisWindowWillCloseIn5Seconds"))
? localeService.t("thisWindowWillCloseIn5Seconds")
: localeService.t("youMayCloseThisWindow");
h1.className = "font-weight-semibold";
@@ -102,11 +123,20 @@ function displayHandoffMessage(client: string) {
});
content.appendChild(button);
// Countdown timer (closes tab upon completion)
let num = Number(p.textContent.match(/\d+/)[0]);
if (p.textContent === null) {
throw new Error("count down container is null");
}
const counterString: string | null = p.textContent.match(/\d+/)?.[0] || null;
if (!counterString) {
throw new Error("count down time cannot be null");
}
let num: number = Number(counterString);
const interval = setInterval(() => {
if (num > 1) {
if (p.textContent === null) {
throw new Error("count down container is null");
}
p.textContent = p.textContent.replace(String(num), String(num - 1));
num--;
} else {

View File

@@ -142,7 +142,7 @@ const plugins = [
new HtmlWebpackPlugin({
template: "./src/connectors/duo-redirect.html",
filename: "duo-redirect-connector.html",
chunks: ["connectors/duo-redirect"],
chunks: ["connectors/duo-redirect", "styles"],
}),
new HtmlWebpackPlugin({
template: "./src/404.html",

View File

@@ -55,6 +55,9 @@ export enum FeatureFlag {
VaultBulkManagementAction = "vault-bulk-management-action",
SecurityTasks = "security-tasks",
CipherKeyEncryption = "cipher-key-encryption",
/* Platform */
IpcChannelFramework = "ipc-channel-framework",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -118,6 +121,9 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.UserKeyRotationV2]: FALSE,
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
/* Platform */
[FeatureFlag.IpcChannelFramework]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -189,6 +189,33 @@ describe("IntegrationContext", () => {
expect(result).toBe("");
});
it("extracts the hostname when extractHostname is true", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website(
{ website: "https://www.example.com/path" },
{ extractHostname: true },
);
expect(result).toBe("www.example.com");
});
it("falls back to the full URL when Utils.getHost cannot extract the hostname", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website({ website: "invalid-url" }, { extractHostname: true });
expect(result).toBe("invalid-url");
});
it("truncates the website to maxLength", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website({ website: "www.example.com" }, { maxLength: 3 });
expect(result).toBe("www");
});
});
describe("generatedBy", () => {
@@ -211,5 +238,15 @@ describe("IntegrationContext", () => {
expect(result).toBe("result");
expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedByWithWebsite", "www.example.com");
});
it("truncates generated text to maxLength", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
i18n.t.mockReturnValue("This is the result text");
const result = context.generatedBy({ website: null }, { maxLength: 4 });
expect(result).toBe("This");
expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedBy", "");
});
});
});

View File

@@ -79,24 +79,40 @@ export class IntegrationContext<Settings extends object> {
/** look up the website the integration is working with.
* @param request supplies information about the state of the extension site
* @param options optional parameters
* @param options.extractHostname when `true`, tries to extract the hostname from the website URL, returns full URL otherwise
* @param options.maxLength limits the length of the return value
* @returns The website or an empty string if a website isn't available
* @remarks `website` is usually supplied when generating a credential from the vault
*/
website(request: IntegrationRequest) {
return request.website ?? "";
website(
request: IntegrationRequest,
options?: { extractHostname?: boolean; maxLength?: number },
) {
let url = request.website ?? "";
if (options?.extractHostname) {
url = Utils.getHost(url) ?? url;
}
return url.slice(0, options?.maxLength);
}
/** look up localized text indicating Bitwarden requested the forwarding address.
* @param request supplies information about the state of the extension site
* @param options optional parameters
* @param options.extractHostname when `true`, extracts the hostname from the website URL
* @param options.maxLength limits the length of the return value
* @returns localized text describing a generated forwarding address
*/
generatedBy(request: IntegrationRequest) {
const website = this.website(request);
generatedBy(
request: IntegrationRequest,
options?: { extractHostname?: boolean; maxLength?: number },
) {
const website = this.website(request, { extractHostname: options?.extractHostname ?? false });
const descriptionId =
website === "" ? "forwarderGeneratedBy" : "forwarderGeneratedByWithWebsite";
const description = this.i18n.t(descriptionId, website);
return description;
return description.slice(0, options?.maxLength);
}
}

View File

@@ -55,6 +55,10 @@ describe("Addy.io forwarder", () => {
const result = AddyIo.forwarder.createForwardingEmail.body(null, context);
expect(context.generatedBy).toHaveBeenCalledWith(null, {
extractHostname: true,
maxLength: 200,
});
expect(result).toEqual({
domain: "domain",
description: "generated by",

View File

@@ -39,7 +39,7 @@ const createForwardingEmail = Object.freeze({
body(request: IntegrationRequest, context: ForwarderContext<AddyIoSettings>) {
return {
domain: context.emailDomain(),
description: context.generatedBy(request),
description: context.generatedBy(request, { extractHostname: true, maxLength: 200 }),
};
},
hasJsonPayload(response: Response) {

View File

@@ -56,6 +56,11 @@ describe("Firefox Relay forwarder", () => {
const result = FirefoxRelay.forwarder.createForwardingEmail.body(null, context);
expect(context.website).toHaveBeenCalledWith(null, { maxLength: 255 });
expect(context.generatedBy).toHaveBeenCalledWith(null, {
extractHostname: true,
maxLength: 64,
});
expect(result).toEqual({
enabled: true,
generated_for: "website",

View File

@@ -33,8 +33,8 @@ const createForwardingEmail = Object.freeze({
body(request: IntegrationRequest, context: ForwarderContext<FirefoxRelaySettings>) {
return {
enabled: true,
generated_for: context.website(request),
description: context.generatedBy(request),
generated_for: context.website(request, { maxLength: 255 }),
description: context.generatedBy(request, { extractHostname: true, maxLength: 64 }),
};
},
hasJsonPayload(response: Response) {