1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

Merge remote-tracking branch 'origin/main' into PM-19741

This commit is contained in:
Jonathan Prusik
2025-05-16 13:52:37 -04:00
692 changed files with 7838 additions and 4098 deletions

5
.github/CODEOWNERS vendored
View File

@@ -185,5 +185,8 @@ apps/web/src/locales/en/messages.json
**/entrypoint.sh
## Overrides
# tsconfig files are potentially dangerous and will be reviewed by platform to prevent misconfigurations
# For the time being platform owns tsconfig and jest config
# These overrides will be removed after Nx is implemented
# To track that effort please see https://bitwarden.atlassian.net/browse/PM-21636
**/tsconfig.json @bitwarden/team-platform-dev
**/jest.config.js @bitwarden/team-platform-dev

View File

@@ -222,7 +222,6 @@
"@types/chrome",
"@types/firefox-webext-browser",
"@types/glob",
"@types/jquery",
"@types/lowdb",
"@types/node",
"@types/node-forge",
@@ -330,9 +329,7 @@
"autoprefixer",
"bootstrap",
"chromatic",
"jquery",
"ngx-toastr",
"popper.js",
"react",
"react-dom",
"remark-gfm",

View File

@@ -8,10 +8,9 @@ name: Build Browser on PR Target
on:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
types: [opened, synchronize, reopened]
branches:
- main
paths:
- 'apps/browser/**'
- 'libs/**'

View File

@@ -8,10 +8,9 @@ name: Build CLI on PR Target
on:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
types: [opened, synchronize, reopened]
branches:
- main
paths:
- 'apps/cli/**'
- 'libs/**'

View File

@@ -9,10 +9,9 @@ name: Build Desktop on PR Target
on:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
types: [opened, synchronize, reopened]
branches:
- main
paths:
- 'apps/desktop/**'
- 'libs/**'

View File

@@ -8,10 +8,9 @@ name: Build Web on PR Target
on:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
types: [opened, synchronize, reopened]
branches:
- main
paths:
- 'apps/web/**'
- 'libs/**'

View File

@@ -73,7 +73,7 @@ jobs:
run: npm run build-storybook:ci
- name: Publish to Chromatic
uses: chromaui/action@8a12962215a66cd05b1ac5b0f1c08768d1aab155 # v11.25.0
uses: chromaui/action@e8cc4c31775280b175a3c440076c00d19a9014d7 # v11.28.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@@ -7,8 +7,14 @@ on:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target:
types: [opened, synchronize]
types: [opened, synchronize, reopened]
branches:
- "main"
jobs:
check-run:

View File

@@ -8,7 +8,7 @@ on:
- "rc"
- "hotfix-rc-*"
pull_request:
types: [opened, synchronize]
types: [ opened, synchronize ]
jobs:
@@ -66,12 +66,15 @@ jobs:
reporter: jest-junit
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
- name: Upload results to codecov.io
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
- name: Upload test coverage
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: jest-coverage
path: ./coverage/lcov.info
rust:
name: Run Rust tests on ${{ matrix.os }}
runs-on: ${{ matrix.os || 'ubuntu-22.04' }}
@@ -148,7 +151,37 @@ jobs:
working-directory: ./apps/desktop/desktop_native
run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage
- name: Upload to codecov.io
- name: Upload test coverage
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: rust-coverage
path: ./apps/desktop/desktop_native/lcov.info
upload-codecov:
name: Upload to Codecov
runs-on: ubuntu-22.04
needs:
- testing
- rust-coverage
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download jest coverage
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: jest-coverage
path: ./
- name: Download rust coverage
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: rust-coverage
path: ./apps/desktop/desktop_native
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
with:
files: ./apps/desktop/desktop_native/lcov.info
files: |
./lcov.info
./apps/desktop/desktop_native/lcov.info

View File

@@ -6,6 +6,32 @@
"analytics": false
},
"projects": {
"bit-web": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "bitwarden_license/bit-web",
"sourceRoot": "bitwarden_license/bit-web/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/web",
"index": "apps/web/src/index.html",
"main": "bitwarden_license/bit-web/src/app/main.ts",
"polyfills": "apps/web/src/polyfills.ts",
"tsConfig": "bitwarden_license/bit-web/tsconfig.json",
"assets": ["apps/web/src/favicon.ico"],
"styles": [],
"scripts": []
}
}
}
},
"web": {
"projectType": "application",
"schematics": {
@@ -22,8 +48,8 @@
"options": {
"outputPath": "dist/web",
"index": "apps/web/src/index.html",
"main": "apps/web/src/app/main.ts",
"polyfills": "apps/web/src/app/polyfills.ts",
"main": "apps/web/src/main.ts",
"polyfills": "apps/web/src/polyfills.ts",
"tsConfig": "apps/web/tsconfig.json",
"assets": ["apps/web/src/favicon.ico"],
"styles": [],

View File

@@ -1071,6 +1071,10 @@
},
"description": "Aria label for the view button in notification bar confirmation message"
},
"notificationNewItemAria": {
"message": "New Item, opens in new window",
"description": "Aria label for the new item button in notification bar confirmation message when error is prompted"
},
"notificationEditTooltip": {
"message": "Edit before saving",
"description": "Tooltip and Aria label for edit button on cipher item"
@@ -1090,13 +1094,24 @@
},
"notificationLoginSaveConfirmation": {
"message": "saved to Bitwarden.",
"description": "Shown to user after item is saved."
},
"notificationLoginUpdatedConfirmation": {
"message": "updated in Bitwarden.",
"description": "Shown to user after item is updated."
},
"selectItemAriaLabel": {
"message": "Select $ITEMTYPE$, $ITEMNAME$",
"description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.",
"placeholders": {
"itemType": {
"content": "$1"
},
"itemName": {
"content": "$2"
}
}
},
"saveAsNewLoginAction": {
"message": "Save as new login",
"description": "Button text for saving login details as a new entry."
@@ -1105,6 +1120,10 @@
"message": "Update login",
"description": "Button text for updating an existing login entry."
},
"unlockToSave": {
"message": "Unlock to save this login",
"description": "User prompt to take action in order to save the login they just entered."
},
"saveLogin": {
"message": "Save login",
"description": "Prompt asking the user if they want to save their login details."
@@ -2208,15 +2227,6 @@
"vaultTimeoutAction1": {
"message": "Timeout action"
},
"newCustomizationOptionsCalloutTitle": {
"message": "New customization options"
},
"newCustomizationOptionsCalloutContent": {
"message": "Customize your vault experience with quick copy actions, compact mode, and more!"
},
"newCustomizationOptionsCalloutLink": {
"message": "View all Appearance settings"
},
"lock": {
"message": "Lock",
"description": "Verb form: to make secure or inaccessible by"
@@ -3621,7 +3631,7 @@
"orgTrustWarning1": {
"message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint."
},
"trustUser":{
"trustUser": {
"message": "Trust user"
},
"sendsNoItemsTitle": {
@@ -5285,6 +5295,9 @@
"secureDevicesBody": {
"message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps."
},
"nudgeBadgeAria": {
"message": "1 notification"
},
"emptyVaultNudgeTitle": {
"message": "Import existing passwords"
},
@@ -5297,8 +5310,14 @@
"hasItemsVaultNudgeTitle": {
"message": "Welcome to your vault!"
},
"hasItemsVaultNudgeBody": {
"message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else"
"hasItemsVaultNudgeBodyOne": {
"message": "Autofill items for the current page"
},
"hasItemsVaultNudgeBodyTwo": {
"message": "Favorite items for easy access"
},
"hasItemsVaultNudgeBodyThree": {
"message": "Search your vault for something else"
},
"newLoginNudgeTitle": {
"message": "Save time with autofill"
@@ -5348,5 +5367,8 @@
"message": "Learn more about SSH agent",
"description": "Two part message",
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
},
"noPermissionsViewPage": {
"message": "You do not have permissions to view this page. Try logging in with a different account."
}
}

View File

@@ -168,18 +168,21 @@ type Story = StoryObj<ExtensionAnonLayoutWrapperComponent>;
@Component({
selector: "bit-default-primary-outlet-example-component",
template: "<p>Primary Outlet Example: <br> your primary component goes here</p>",
standalone: false,
})
class DefaultPrimaryOutletExampleComponent {}
@Component({
selector: "bit-default-secondary-outlet-example-component",
template: "<p>Secondary Outlet Example: <br> your secondary component goes here</p>",
standalone: false,
})
class DefaultSecondaryOutletExampleComponent {}
@Component({
selector: "bit-default-env-selector-outlet-example-component",
template: "<p>Env Selector Outlet Example: <br> your env selector component goes here</p>",
standalone: false,
})
class DefaultEnvSelectorOutletExampleComponent {}
@@ -264,6 +267,7 @@ const changedData: ExtensionAnonLayoutWrapperData = {
template: `
<button type="button" bitButton buttonType="primary" (click)="toggleData()">Toggle Data</button>
`,
standalone: false,
})
export class DynamicContentExampleComponent {
initialData = true;

View File

@@ -5,5 +5,6 @@ import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/ang
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
standalone: false,
})
export class SetPasswordComponent extends BaseSetPasswordComponent {}

View File

@@ -18,5 +18,6 @@ import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@b
useExisting: VaultTimeoutInputComponent,
},
],
standalone: false,
})
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {}

View File

@@ -8,6 +8,7 @@ import { postLogoutMessageListener$ } from "./utils/post-logout-message-listener
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
standalone: false,
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
onSuccessfulChangePassword: () => Promise<void> = this.doOnSuccessfulChangePassword.bind(this);

View File

@@ -946,9 +946,7 @@ export default class NotificationBackground {
private async getDecryptedCipherById(cipherId: string, userId: UserId) {
const cipher = await this.cipherService.get(cipherId, userId);
if (cipher != null && cipher.type === CipherType.Login) {
return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
);
return await this.cipherService.decrypt(cipher, userId);
}
return null;
}

View File

@@ -74,6 +74,10 @@ const actionButtonStyles = ({
background-color: ${themes[theme].primary["700"]};
color: ${themes[theme].text.contrast};
}
:focus {
outline: 2px solid ${themes[theme].primary["600"]};
outline-offset: 1px;
}
`}
svg {

View File

@@ -8,15 +8,19 @@ import { border, themes, typography, spacing } from "../constants/styles";
export type BadgeButtonProps = {
buttonAction: (e: Event) => void;
buttonText: string;
itemName: string;
disabled?: boolean;
theme: Theme;
username?: string;
};
export function BadgeButton({
buttonAction,
buttonText,
disabled = false,
itemName,
theme,
username,
}: BadgeButtonProps) {
const handleButtonClick = (event: Event) => {
if (!disabled) {
@@ -28,6 +32,7 @@ export function BadgeButton({
<button
type="button"
title=${buttonText}
aria-label=${[buttonText, [itemName, username].filter(Boolean).join(" ")]}
class=${badgeButtonStyles({ disabled, theme })}
@click=${handleButtonClick}
>
@@ -65,5 +70,9 @@ const badgeButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Them
background-color: ${themes[theme].primary["600"]};
color: ${themes[theme].text.contrast};
}
:focus {
outline: 2px solid ${themes[theme].primary["600"]};
outline-offset: 2px;
}
`}
`;

View File

@@ -3,17 +3,24 @@ import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { I18n } from "../common-types";
import { spacing, themes } from "../constants/styles";
import { Close as CloseIcon } from "../icons";
export type CloseButtonProps = {
i18n: I18n;
handleCloseNotification: (e: Event) => void;
theme: Theme;
};
export function CloseButton({ handleCloseNotification, theme }: CloseButtonProps) {
export function CloseButton({ handleCloseNotification, i18n, theme }: CloseButtonProps) {
return html`
<button type="button" class=${closeButtonStyles(theme)} @click=${handleCloseNotification}>
<button
type="button"
aria-label=${i18n.close}
class=${closeButtonStyles(theme)}
@click=${handleCloseNotification}
>
${CloseIcon({ theme })}
</button>
`;

View File

@@ -33,6 +33,9 @@ export function OptionSelectionButton({
class=${selectionButtonStyles({ disabled, toggledOn, theme })}
title=${text}
type="button"
aria-haspopup="menu"
aria-expanded=${toggledOn}
aria-controls="option-menu"
@click=${handleButtonClick}
>
${buttonIcon ?? nothing}

View File

@@ -8,8 +8,10 @@ import { I18n } from "../common-types";
export type CipherActionProps = {
handleAction?: (e: Event) => void;
i18n: I18n;
itemName: string;
notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add;
theme: Theme;
username?: string;
};
export function CipherAction({
@@ -17,14 +19,18 @@ export function CipherAction({
/* no-op */
},
i18n,
itemName,
notificationType,
theme,
username,
}: CipherActionProps) {
return notificationType === NotificationTypes.Change
? BadgeButton({
buttonAction: handleAction,
buttonText: i18n.notificationUpdate,
itemName,
theme,
username,
})
: EditButton({
buttonAction: handleAction,

View File

@@ -32,14 +32,21 @@ export function CipherItem({
notificationType,
theme = ThemeTypes.Light,
}: CipherItemProps) {
const { icon } = cipher;
const { icon, name, login } = cipher;
const uri = (icon.imageEnabled && icon.image) || undefined;
let cipherActionButton = null;
if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) {
cipherActionButton = html`<div>
${CipherAction({ handleAction, i18n, notificationType, theme })}
${CipherAction({
handleAction,
i18n,
itemName: name,
notificationType,
theme,
username: login?.username,
})}
</div>`;
}

View File

@@ -11,6 +11,7 @@ export type IconProps = {
color?: string;
disabled?: boolean;
theme: Theme;
ariaHidden?: boolean;
};
export type Option = {

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function AngleDown({ color, disabled, theme }: IconProps) {
export function AngleDown({ ariaHidden = true, 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 8" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 8"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M13.53.47a.75.75 0 0 0-1.06 0L7 5.94 1.53.47A.75.75 0 1 0 .47 1.53l6 6a.75.75 0 0 0 1.06 0l6-6a.75.75 0 0 0 0-1.06Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function AngleUp({ color, disabled, theme }: IconProps) {
export function AngleUp({ ariaHidden = true, 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 8" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 8"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M.47 7.53a.75.75 0 0 0 1.06 0L7 2.06l5.47 5.47a.75.75 0 1 0 1.06-1.06l-6-6a.75.75 0 0 0-1.06 0l-6 6a.75.75 0 0 0 0 1.06Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function Business({ color, disabled, theme }: IconProps) {
export function Business({ ariaHidden = true, 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 12 16" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 16"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M3.25 3a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5ZM7.25 3a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5ZM7.25 6a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5ZM6.5 9.75A.75.75 0 0 1 7.25 9h1.5a.75.75 0 0 1 0 1.5h-1.5a.75.75 0 0 1-.75-.75ZM2.5 6.75A.75.75 0 0 1 3.25 6h1.5a.75.75 0 0 1 0 1.5h-1.5a.75.75 0 0 1-.75-.75ZM3.25 9a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function Close({ color, disabled, theme }: IconProps) {
export function Close({ ariaHidden = true, 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">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M.22.22a.75.75 0 0 1 1.06 0L7 5.94 12.72.22a.75.75 0 1 1 1.06 1.06L8.06 7l5.72 5.72a.75.75 0 1 1-1.06 1.06L7 8.06l-5.72 5.72a.75.75 0 0 1-1.06-1.06L5.94 7 .22 1.28a.75.75 0 0 1 0-1.06Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function CollectionShared({ color, disabled, theme }: IconProps) {
export function CollectionShared({ ariaHidden = true, 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">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M3.5.75A.75.75 0 0 1 4.25 0h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 3.5.75ZM2.25 2a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5ZM6 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM10 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 11.46a1.928 1.928 0 0 0-.586-1.386 2.035 2.035 0 0 0-2.828 0A1.928 1.928 0 0 0 3 11.461c0 .298.241.539.54.539h2.92a.54.54 0 0 0 .54-.54ZM8 11.46a2.928 2.928 0 0 0-.371-1.426A2.005 2.005 0 0 1 9 9.5a2.035 2.035 0 0 1 1.414.574A1.928 1.928 0 0 1 11 11.461a.54.54 0 0 1-.54.539H7.904c.063-.168.097-.35.097-.54Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function ExclamationTriangle({ color, disabled, theme }: IconProps) {
export function ExclamationTriangle({ ariaHidden = true, 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 16 15" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 15"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M9 11C9 11.5523 8.55229 12 8 12C7.44772 12 7 11.5523 7 11C7 10.4477 7.44772 10 8 10C8.55229 10 9 10.4477 9 11Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function ExternalLink({ color, disabled, theme }: IconProps) {
export function ExternalLink({ ariaHidden = true, 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">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
aria-hidden="${ariaHidden}"
>
<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"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function Family({ color, disabled, theme }: IconProps) {
export function Family({ ariaHidden = true, 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 16 16" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
fill-rule="evenodd"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function Folder({ color, disabled, theme }: IconProps) {
export function Folder({ ariaHidden = true, 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 16 13" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 13"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M2 0a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8L6.586.586A2 2 0 0 0 5.172 0H2Zm5.379 3.5L5.525 1.646a.5.5 0 0 0-.353-.146H2a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V4a.5.5 0 0 0-.5-.5H7.379Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function Globe({ color, disabled, theme }: IconProps) {
export function Globe({ ariaHidden = true, 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 16 16" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm0 14.5c.23 0 .843-.226 1.487-1.514.524-1.048.906-2.526.994-4.236H5.519c.088 1.71.47 3.188.994 4.236C7.157 14.274 7.77 14.5 8 14.5ZM5.52 7.25h4.96c-.087-1.71-.47-3.188-.993-4.236C8.843 1.726 8.23 1.5 8 1.5c-.23 0-.843.226-1.487 1.514C5.99 4.062 5.607 5.54 5.52 7.25Zm6.463 0h2.474a6.506 6.506 0 0 0-3.766-5.168c.718 1.305 1.197 3.125 1.292 5.168Zm-7.966 0c.095-2.043.574-3.863 1.292-5.168A6.506 6.506 0 0 0 1.543 7.25h2.474Zm7.966 1.5c-.095 2.043-.574 3.863-1.292 5.168a6.506 6.506 0 0 0 3.766-5.168h-2.474Zm-6.677 5.185c-.718-1.305-1.197-3.125-1.292-5.168H1.54a6.506 6.506 0 0 0 3.766 5.168Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function PencilSquare({ color, disabled, theme }: IconProps) {
export function PencilSquare({ ariaHidden = true, 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 15 15" fill="none" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M11.013.677a1.75 1.75 0 0 1 2.474 0l.836.836a1.75 1.75 0 0 1 0 2.475L9.03 9.28a.75.75 0 0 1-.348.197l-3 .75a.75.75 0 0 1-.91-.91l.75-3a.75.75 0 0 1 .198-.348L11.013.677Zm1.414 1.06a.25.25 0 0 0-.354 0l-.646.647a.75.75 0 0 1 .103.086l1 1a.751.751 0 0 1 .087.103l.646-.646a.25.25 0 0 0 0-.353l-.836-.836Zm-.854 2.88a.752.752 0 0 1-.103-.087l-1-1a.756.756 0 0 1-.087-.103L6.928 6.884 6.531 8.47l1.586-.397 3.456-3.456Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function Shield({ color, theme }: IconProps) {
export function Shield({ ariaHidden = true, color, theme }: IconProps) {
const shapeColor = color || themes[theme].brandLogo;
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 16"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M13.469.2A.647.647 0 0 0 13 0H1a.639.639 0 0 0-.468.2.641.641 0 0 0-.2.468v8a4.81 4.81 0 0 0 .348 1.777c.216.557.507 1.083.865 1.563.367.48.779.925 1.229 1.329.417.383.857.741 1.317 1.073.4.284.82.553 1.26.807.44.254.75.425.932.515.183.09.33.16.44.208.087.041.181.062.277.06a.58.58 0 0 0 .27-.063c.113-.05.259-.118.444-.208s.5-.262.932-.515c.432-.253.857-.523 1.26-.807.46-.332.9-.69 1.319-1.073.45-.404.861-.849 1.228-1.33.357-.48.648-1.005.865-1.562a4.79 4.79 0 0 0 .348-1.777v-8A.63.63 0 0 0 13.47.2Zm-1.547 8.54c0 2.9-4.921 5.392-4.921 5.392V1.714h4.92v7.027Z"

View File

@@ -4,11 +4,16 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
export function User({ color, disabled, theme }: IconProps) {
export function User({ ariaHidden = true, 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 15" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 15"
fill="none"
aria-hidden="${ariaHidden}"
>
<path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M9.203 7.339a4 4 0 1 0-4.407 0A7.033 7.033 0 0 0 2.05 8.953a6.655 6.655 0 0 0-1.517 2.162A6.393 6.393 0 0 0 0 13.667C0 14.403.597 15 1.333 15h11.334c.736 0 1.333-.597 1.333-1.333 0-.876-.181-1.743-.533-2.552a6.654 6.654 0 0 0-1.517-2.162 7.032 7.032 0 0 0-2.747-1.614ZM9.5 4a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Zm2.592 7.714c.247.57.384 1.175.405 1.786H1.503a4.897 4.897 0 0 1 .405-1.786 5.156 5.156 0 0 1 1.177-1.675 5.534 5.534 0 0 1 1.787-1.136A5.805 5.805 0 0 1 7 8.5c.732 0 1.456.137 2.128.403.673.265 1.28.652 1.787 1.136a5.156 5.156 0 0 1 1.177 1.675Z"

View File

@@ -3,6 +3,7 @@ import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CloseButton, CloseButtonProps } from "../../buttons/close-button";
import { mockI18n } from "../mock-data";
export default {
title: "Components/Buttons/Close Button",
@@ -15,6 +16,7 @@ export default {
handleCloseNotification: () => {
alert("Close button clicked!");
},
i18n: mockI18n,
},
parameters: {
design: {

View File

@@ -131,12 +131,15 @@ export const mockI18n = {
notificationUnlock: "Unlock",
notificationUnlockDesc: "Unlock your Bitwarden vault to complete the autofill request.",
notificationViewAria: `View $ITEMNAME$, opens in new window`,
notificationNewItemAria: "New Item, opens in new window",
saveAction: "Save",
saveAsNewLoginAction: "Save as new login",
saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.",
saveLogin: "Save login",
selectItemAriaLabel: "Select $ITEMTYPE$, $ITEMNAME$",
typeLogin: "Login",
unlockToSave: "Unlock to save this login",
updateLoginAction: "Update login",
updateLogin: "Update existing login",
vault: "Vault",

View File

@@ -48,6 +48,7 @@ export function NotificationConfirmationBody({
? NotificationConfirmationMessage({
buttonAria,
buttonText,
error,
itemName,
message: confirmationMessage,
messageDetails,
@@ -62,7 +63,7 @@ export function NotificationConfirmationBody({
export const iconContainerStyles = (error?: string | boolean) => css`
> svg {
width: ${!error ? "50px" : "40px"};
height: fit-content;
height: auto;
}
`;
export const notificationConfirmationBodyStyles = ({ theme }: { theme: Theme }) => css`

View File

@@ -45,7 +45,9 @@ export function NotificationConfirmationContainer({
const headerMessage = getHeaderMessage(i18n, type, error);
const confirmationMessage = getConfirmationMessage(i18n, type, error);
const buttonText = error ? i18n.newItem : i18n.view;
const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]);
const buttonAria = error
? i18n.notificationNewItemAria
: chrome.i18n.getMessage("notificationViewAria", [itemName]);
let messageDetails: string | undefined;
let remainingTasksCount: number | undefined;
@@ -68,6 +70,7 @@ export function NotificationConfirmationContainer({
<div class=${notificationContainerStyles(theme)}>
${NotificationHeader({
handleCloseNotification,
i18n,
message: headerMessage,
theme,
})}

View File

@@ -8,6 +8,7 @@ import { spacing, themes, typography } from "../../constants/styles";
export type NotificationConfirmationMessageProps = {
buttonAria?: string;
buttonText?: string;
error?: string;
itemName?: string;
message?: string;
messageDetails?: string;
@@ -18,6 +19,7 @@ export type NotificationConfirmationMessageProps = {
export function NotificationConfirmationMessage({
buttonAria,
buttonText,
error,
itemName,
message,
messageDetails,
@@ -29,7 +31,11 @@ export function NotificationConfirmationMessage({
${message || buttonText
? html`
<div class=${singleLineWrapperStyles}>
<span class=${itemNameStyles(theme)} title=${itemName}> ${itemName} </span>
${!error && itemName
? html`
<span class=${itemNameStyles(theme)} title=${itemName}> ${itemName} </span>
`
: nothing}
<span
title=${message || buttonText}
class=${notificationConfirmationMessageStyles(theme)}

View File

@@ -1,5 +1,5 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { html, nothing } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
@@ -47,14 +47,14 @@ export function NotificationContainer({
type,
}: NotificationContainerProps) {
const headerMessage = getHeaderMessage(i18n, type);
const showBody = true;
const showBody = type !== NotificationTypes.Unlock;
return html`
<div class=${notificationContainerStyles(theme)}>
${NotificationHeader({
handleCloseNotification,
i18n,
message: headerMessage,
standalone: showBody,
theme,
})}
${showBody
@@ -65,7 +65,7 @@ export function NotificationContainer({
theme,
i18n,
})
: null}
: nothing}
${NotificationFooter({
handleSaveAction,
collections,
@@ -106,7 +106,7 @@ function getHeaderMessage(i18n: I18n, type?: NotificationType) {
case NotificationTypes.Change:
return i18n.updateLogin;
case NotificationTypes.Unlock:
return "";
return i18n.unlockToSave;
default:
return undefined;
}

View File

@@ -35,7 +35,13 @@ export function NotificationFooter({
handleSaveAction,
}: NotificationFooterProps) {
const isChangeNotification = notificationType === NotificationTypes.Change;
const primaryButtonText = i18n.saveAction;
const isUnlockNotification = notificationType === NotificationTypes.Unlock;
let primaryButtonText = i18n.saveAction;
if (isUnlockNotification) {
primaryButtonText = i18n.notificationUnlock;
}
return html`
<div class=${[displayFlex, notificationFooterStyles({ isChangeNotification })]}>

View File

@@ -4,6 +4,7 @@ import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { CloseButton } from "../buttons/close-button";
import { I18n } from "../common-types";
import { spacing, themes } from "../constants/styles";
import { BrandIconContainer } from "../icons/brand-icon-container";
@@ -16,6 +17,7 @@ const { css } = createEmotion({
});
export type NotificationHeaderProps = {
i18n: I18n;
message?: string;
standalone?: boolean;
theme: Theme;
@@ -23,6 +25,7 @@ export type NotificationHeaderProps = {
};
export function NotificationHeader({
i18n,
message,
standalone = false,
theme = ThemeTypes.Light,
@@ -35,7 +38,7 @@ export function NotificationHeader({
<div class=${notificationHeaderStyles({ standalone, theme })}>
${showIcon ? BrandIconContainer({ theme }) : null}
${message ? NotificationHeaderMessage({ message, theme }) : null}
${isDismissable ? CloseButton({ handleCloseNotification, theme }) : null}
${isDismissable ? CloseButton({ handleCloseNotification, i18n, theme }) : null}
</div>
`;
}
@@ -56,8 +59,8 @@ const notificationHeaderStyles = ({
white-space: nowrap;
${standalone
? css`
? css``
: css`
border-bottom: 0.5px solid ${themes[theme].secondary["300"]};
`
: css``}
`}
`;

View File

@@ -13,11 +13,19 @@ const { css } = createEmotion({
});
export type OptionItemProps = Option & {
contextLabel?: string;
theme: Theme;
handleSelection: () => void;
};
export function OptionItem({ icon, text, value, theme, handleSelection }: OptionItemProps) {
export function OptionItem({
contextLabel,
icon,
text,
theme,
value,
handleSelection,
}: OptionItemProps) {
const handleSelectionKeyUpProxy = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["Enter", "Space"]);
if (listenedForKeys.has(event.code) && event.target instanceof Element) {
@@ -29,12 +37,18 @@ export function OptionItem({ icon, text, value, theme, handleSelection }: Option
const iconProps: IconProps = { color: themes[theme].text.main, theme };
const itemIcon = icon?.(iconProps);
const ariaLabel =
contextLabel && text
? chrome.i18n.getMessage("selectItemAriaLabel", [contextLabel, text])
: text;
return html`<div
class=${optionItemStyles}
key=${value}
tabindex="0"
title=${text}
role="option"
aria-label=${ariaLabel}
@click=${handleSelection}
@keyup=${handleSelectionKeyUpProxy}
>

View File

@@ -33,17 +33,41 @@ export function OptionItems({
const isSafari = false;
return html`
<div class=${optionsStyles({ theme, topOffset })} key="container">
<div
class=${optionsStyles({ theme, topOffset })}
key="container"
@keyup=${(e: KeyboardEvent) => handleMenuKeyUp(e)}
>
${label ? html`<div class=${optionsLabelStyles({ theme })}>${label}</div>` : nothing}
<div class=${optionsWrapper({ isSafari, theme })}>
${options.map((option) =>
OptionItem({ ...option, theme, handleSelection: () => handleOptionSelection(option) }),
OptionItem({
...option,
theme,
contextLabel: label,
handleSelection: () => handleOptionSelection(option),
}),
)}
</div>
</div>
`;
}
function handleMenuKeyUp(event: KeyboardEvent) {
const items = [
...(event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>('[tabindex="0"]'),
];
const index = items.indexOf(document.activeElement as HTMLElement);
const direction = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
if (index === -1 || direction === 0) {
return;
}
event.preventDefault();
items[(index + direction + items.length) % items.length]?.focus();
}
const optionsStyles = ({ theme, topOffset }: { theme: Theme; topOffset: number }) => css`
${typography.body1}

View File

@@ -48,10 +48,18 @@ export class OptionSelection extends LitElement {
@state()
private selection?: Option;
private handleButtonClick = (event: Event) => {
private static currentOpenInstance: OptionSelection | null = null;
private handleButtonClick = async (event: Event) => {
if (!this.disabled) {
// Menu is about to be shown
if (!this.showMenu) {
const isOpening = !this.showMenu;
if (isOpening) {
if (OptionSelection.currentOpenInstance && OptionSelection.currentOpenInstance !== this) {
OptionSelection.currentOpenInstance.showMenu = false;
}
OptionSelection.currentOpenInstance = this;
this.menuTopOffset = this.offsetTop;
// Distance from right edge of button to left edge of the viewport
@@ -71,9 +79,29 @@ export class OptionSelection extends LitElement {
optionsMenuItemMaxWidth + optionItemIconWidth + 2 + 8 + 12 * 2;
this.menuIsEndJustified = distanceFromViewportRightEdge < maxDifferenceThreshold;
} else {
if (OptionSelection.currentOpenInstance === this) {
OptionSelection.currentOpenInstance = null;
}
}
this.showMenu = !this.showMenu;
this.showMenu = isOpening;
if (this.showMenu) {
await this.updateComplete;
const firstItem = this.querySelector('#option-menu [tabindex="0"]') as HTMLElement;
firstItem?.focus();
}
}
};
private handleFocusOut = (event: FocusEvent) => {
const relatedTarget = event.relatedTarget;
if (!(relatedTarget instanceof Node) || !this.contains(relatedTarget)) {
this.showMenu = false;
if (OptionSelection.currentOpenInstance === this) {
OptionSelection.currentOpenInstance = null;
}
}
};
@@ -95,7 +123,10 @@ export class OptionSelection extends LitElement {
}
return html`
<div class=${optionSelectionStyles({ menuIsEndJustified: this.menuIsEndJustified })}>
<div
class=${optionSelectionStyles({ menuIsEndJustified: this.menuIsEndJustified })}
@focusout=${this.handleFocusOut}
>
${OptionSelectionButton({
disabled: this.disabled,
icon: this.selection?.icon,

View File

@@ -19,6 +19,7 @@ import {
NotificationBarWindowMessage,
NotificationBarIframeInitData,
NotificationType,
NotificationTypes,
} from "./abstractions/notification-bar";
const logService = new ConsoleLogService(false);
@@ -89,6 +90,7 @@ function getI18n() {
saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"),
saveLogin: chrome.i18n.getMessage("saveLogin"),
typeLogin: chrome.i18n.getMessage("typeLogin"),
unlockToSave: chrome.i18n.getMessage("unlockToSave"),
updateLogin: chrome.i18n.getMessage("updateLogin"),
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
vault: chrome.i18n.getMessage("vault"),
@@ -154,6 +156,26 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
// Current implementations utilize a require for scss files which creates the need to remove the node.
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
if (isVaultLocked) {
return render(
NotificationContainer({
...notificationBarIframeInitData,
type: NotificationTypes.Unlock,
theme: resolvedTheme,
personalVaultIsAllowed: !personalVaultDisallowed,
handleCloseNotification,
handleSaveAction: (e) => {
sendSaveCipherMessage(true);
// @TODO can't close before vault has finished decrypting, but can't leave open during long decrypt because it looks like the experience has failed
},
handleEditOrUpdateAction,
i18n,
}),
document.body,
);
}
const orgId = selectedVaultSignal.get();
await Promise.all([

View File

@@ -20,7 +20,7 @@ export class OverlayNotificationsContentService
private notificationBarIframeElement: HTMLIFrameElement | null = null;
private currentNotificationBarType: string | null = null;
private removeTabFromNotificationQueueTypes = new Set(["add", "change"]);
private notificationRefreshFlag: boolean;
private notificationRefreshFlag: boolean = false;
private notificationBarElementStyles: Partial<CSSStyleDeclaration> = {
height: "82px",
width: "430px",
@@ -60,6 +60,7 @@ export class OverlayNotificationsContentService
void sendExtensionMessage("checkNotificationQueue");
void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => {
this.notificationRefreshFlag = !!notificationRefreshFlag;
this.setNotificationRefreshBarHeight();
});
}
@@ -228,15 +229,31 @@ export class OverlayNotificationsContentService
this.notificationBarElement.id = "bit-notification-bar";
setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true);
if (this.notificationRefreshFlag) {
setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true);
}
this.setNotificationRefreshBarHeight();
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
}
}
/**
* Sets the height of the notification bar based on the value of `notificationRefreshFlag`.
* If the flag is `true`, the bar is expanded to 400px and aligned right.
* If the flag is `false`, `null`, or `undefined`, it defaults to height of 82px.
* Skips if the notification bar element has not yet been created.
*
*/
private setNotificationRefreshBarHeight() {
const isNotificationV3 = !!this.notificationRefreshFlag;
if (!this.notificationBarElement) {
return;
}
if (isNotificationV3) {
setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true);
}
}
/**
* Sets up the message listener for the initialization of the notification bar.
* This will send the initialization data to the notification bar iframe.

View File

@@ -216,9 +216,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
return this.cipherService.decrypt(cipher, activeUserId);
}),
);
@@ -237,9 +235,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
return this.cipherService.decrypt(cipher, activeUserId);
}),
);

View File

@@ -4,14 +4,14 @@ import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
FormBuilder,
FormGroup,
FormControl,
} from "@angular/forms";
import { RouterModule } from "@angular/router";
import { Observable, filter, firstValueFrom, map, switchMap } from "rxjs";
import { filter, firstValueFrom, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -55,7 +55,7 @@ import {
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { SpotlightComponent, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
import { NudgesService, NudgeType, SpotlightComponent } from "@bitwarden/vault";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
@@ -108,9 +108,7 @@ export class AutofillComponent implements OnInit {
protected showSpotlightNudge$: Observable<boolean> = this.accountService.activeAccount$.pipe(
filter((account): account is Account => account !== null),
switchMap((account) =>
this.vaultNudgesService
.showNudge$(VaultNudgeType.AutofillNudge, account.id)
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed)),
this.nudgesService.showNudgeSpotlight$(NudgeType.AutofillNudge, account.id),
),
);
@@ -155,7 +153,7 @@ export class AutofillComponent implements OnInit {
private configService: ConfigService,
private formBuilder: FormBuilder,
private destroyRef: DestroyRef,
private vaultNudgesService: VaultNudgesService,
private nudgesService: NudgesService,
private accountService: AccountService,
private autofillBrowserSettingsService: AutofillBrowserSettingsService,
) {
@@ -343,8 +341,8 @@ export class AutofillComponent implements OnInit {
}
async dismissSpotlight() {
await this.vaultNudgesService.dismissNudge(
VaultNudgeType.AutofillNudge,
await this.nudgesService.dismissNudge(
NudgeType.AutofillNudge,
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
);
}

View File

@@ -183,6 +183,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@@ -199,6 +200,7 @@ import {
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
@@ -408,6 +410,7 @@ export default class MainBackground {
endUserNotificationService: EndUserNotificationService;
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
taskService: TaskService;
cipherEncryptionService: CipherEncryptionService;
ipcContentScriptManagerService: IpcContentScriptManagerService;
ipcService: IpcService;
@@ -856,6 +859,11 @@ export default class MainBackground {
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
this.cipherEncryptionService = new DefaultCipherEncryptionService(
this.sdkService,
this.logService,
);
this.cipherService = new CipherService(
this.keyService,
this.domainSettingsService,
@@ -871,6 +879,7 @@ export default class MainBackground {
this.stateProvider,
this.accountService,
this.logService,
this.cipherEncryptionService,
);
this.folderService = new FolderService(
this.keyService,

View File

@@ -5,5 +5,6 @@ import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitward
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
standalone: false,
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

View File

@@ -50,17 +50,40 @@ export class PopupSizeService {
PopupSizeService.setStyle(width);
localStorage.setItem(PopupSizeService.LocalStorageKey, width);
});
}
async setHeight() {
const isInChromeTab = await BrowserPopupUtils.isInTab();
/**
* To support both browser default zoom and system default zoom, we need to take into account
* the full screen height. When system default zoom is >100%, window.innerHeight still outputs
* a height equivalent to what it would be at 100%, which can cause the extension window to
* render as too tall. So if the screen height is smaller than the max possible extension height,
* we should use that to set our extension height. Otherwise, we want to use the window.innerHeight
* to support browser zoom.
*
* This is basically a workaround for what we consider a bug with browsers reporting the wrong
* available innerHeight when system zoom is turned on. If that gets fixed, we can remove the code
* checking the screen height.
*/
const MAX_EXT_HEIGHT = 600;
const extensionInnerHeight = window.innerHeight;
// Use a 100px offset when calculating screen height to account for browser container elements
const screenAvailHeight = window.screen.availHeight - 100;
const availHeight =
screenAvailHeight < MAX_EXT_HEIGHT ? screenAvailHeight : extensionInnerHeight;
if (!BrowserPopupUtils.inPopup(window) || isInChromeTab) {
window.document.body.classList.add("body-full");
} else if (window.innerHeight < 400) {
window.document.body.classList.add("body-xxs");
} else if (window.innerHeight < 500) {
window.document.body.classList.add("body-xs");
} else if (window.innerHeight < 600) {
window.document.body.classList.add("body-sm");
window.document.documentElement.classList.add("body-full");
} else if (availHeight < 300) {
window.document.documentElement.classList.add("body-3xs");
} else if (availHeight < 400) {
window.document.documentElement.classList.add("body-xxs");
} else if (availHeight < 500) {
window.document.documentElement.classList.add("body-xs");
} else if (availHeight < 600) {
window.document.documentElement.classList.add("body-sm");
}
}

View File

@@ -13,7 +13,10 @@ import { PopupRouterCacheService, popupRouterCacheGuard } from "./popup-router-c
const flushPromises = async () => await new Promise(process.nextTick);
@Component({ template: "" })
@Component({
template: "",
standalone: false,
})
export class EmptyComponent {}
describe("Popup router cache guard", () => {

View File

@@ -19,10 +19,16 @@ import {
import { PopupViewCacheService } from "./popup-view-cache.service";
@Component({ template: "" })
@Component({
template: "",
standalone: false,
})
export class EmptyComponent {}
@Component({ template: "" })
@Component({
template: "",
standalone: false,
})
export class TestComponent {
private viewCacheService = inject(PopupViewCacheService);

View File

@@ -26,6 +26,7 @@ import {
import { BiometricsService, BiometricStateService } from "@bitwarden/key-management";
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
@@ -42,6 +43,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
</div>
<bit-toast-container></bit-toast-container>
`,
standalone: false,
})
export class AppComponent implements OnInit, OnDestroy {
private compactModeService = inject(PopupCompactModeService);
@@ -71,6 +73,7 @@ export class AppComponent implements OnInit, OnDestroy {
private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
private deviceTrustToastService: DeviceTrustToastService,
private popupSizeService: PopupSizeService,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
}
@@ -79,6 +82,7 @@ export class AppComponent implements OnInit, OnDestroy {
initPopupClosedListener();
this.compactModeService.init();
await this.popupSizeService.setHeight();
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
// Clear them aggressively to make sure this doesn't occur

View File

@@ -22,5 +22,6 @@ import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/a
transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
]),
],
standalone: false,
})
export class UserVerificationComponent extends BaseComponent {}

View File

@@ -8,6 +8,34 @@
html {
overflow: hidden;
min-height: 600px;
height: 100%;
&.body-sm {
min-height: 500px;
}
&.body-xs {
min-height: 400px;
}
&.body-xxs {
min-height: 300px;
}
&.body-3xs {
min-height: 240px;
}
&.body-full {
min-height: unset;
width: 100%;
height: 100%;
& body {
width: 100%;
}
}
}
html,
@@ -20,9 +48,9 @@ body {
body {
width: 380px;
height: 600px;
height: 100%;
position: relative;
min-height: 100vh;
min-height: inherit;
overflow: hidden;
color: $text-color;
background-color: $background-color;
@@ -31,23 +59,6 @@ body {
color: themed("textColor");
background-color: themed("backgroundColor");
}
&.body-sm {
height: 500px;
}
&.body-xs {
height: 400px;
}
&.body-xxs {
height: 300px;
}
&.body-full {
width: 100%;
height: 100%;
}
}
h1,

View File

@@ -6,18 +6,19 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Icons } from "@bitwarden/components";
import { VaultNudgesService } from "@bitwarden/vault";
import { NudgesService } from "@bitwarden/vault";
import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component";
@Component({
selector: "app-tabs-v2",
templateUrl: "./tabs-v2.component.html",
standalone: false,
})
export class TabsV2Component {
private hasActiveBadges$ = this.accountService.activeAccount$
.pipe(getUserId)
.pipe(switchMap((userId) => this.vaultNudgesService.hasActiveBadges$(userId)));
.pipe(switchMap((userId) => this.nudgesService.hasActiveBadges$(userId)));
protected navButtons$: Observable<NavButton[]> = combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
this.hasActiveBadges$,
@@ -53,7 +54,7 @@ export class TabsV2Component {
}),
);
constructor(
private vaultNudgesService: VaultNudgesService,
private nudgesService: NudgesService,
private accountService: AccountService,
private readonly configService: ConfigService,
) {}

View File

@@ -23,6 +23,7 @@
*ngIf="!isBrowserAutofillSettingOverridden && (showAutofillBadge$ | async)"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1</span
>
</div>
@@ -40,7 +41,7 @@
<a
bit-item-content
routerLink="/vault-settings"
(click)="dismissBadge(VaultNudgeType.EmptyVaultNudge)"
(click)="dismissBadge(NudgeType.EmptyVaultNudge)"
>
<i slot="start" class="bwi bwi-vault" aria-hidden="true"></i>
<div class="tw-flex tw-items-center tw-justify-center">
@@ -50,9 +51,10 @@
Will make this dynamic when more nudges are added
-->
<span
*ngIf="!(showVaultBadge$ | async)?.hasBadgeDismissed"
*ngIf="showVaultBadge$ | async"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1</span
>
</div>
@@ -80,9 +82,10 @@
<div class="tw-flex tw-items-center tw-justify-center">
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
<span
*ngIf="(downloadBitwardenNudgeStatus$ | async)?.hasBadgeDismissed === false"
*ngIf="downloadBitwardenNudgeStatus$ | async"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1
</span>
</div>

View File

@@ -17,7 +17,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "@bitwarden/common/types/guid";
import { BadgeComponent, ItemModule } from "@bitwarden/components";
import { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
import { NudgesService, NudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
@@ -42,7 +42,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
],
})
export class SettingsV2Component implements OnInit {
VaultNudgeType = VaultNudgeType;
NudgeType = NudgeType;
activeUserId: UserId | null = null;
protected isBrowserAutofillSettingOverridden = false;
@@ -51,15 +51,15 @@ export class SettingsV2Component implements OnInit {
shareReplay({ bufferSize: 1, refCount: true }),
);
downloadBitwardenNudgeStatus$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe(
downloadBitwardenNudgeStatus$: Observable<boolean> = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id),
this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
),
);
showVaultBadge$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe(
showVaultBadge$: Observable<boolean> = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, account.id),
this.nudgesService.showNudgeBadge$(NudgeType.EmptyVaultNudge, account.id),
),
);
@@ -68,9 +68,9 @@ export class SettingsV2Component implements OnInit {
this.authenticatedAccount$,
]).pipe(
switchMap(([defaultBrowserAutofillDisabled, account]) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.AutofillNudge, account.id).pipe(
map((nudgeStatus) => {
return !defaultBrowserAutofillDisabled && nudgeStatus.hasBadgeDismissed === false;
this.nudgesService.showNudgeBadge$(NudgeType.AutofillNudge, account.id).pipe(
map((badgeStatus) => {
return !defaultBrowserAutofillDisabled && badgeStatus;
}),
),
),
@@ -81,7 +81,7 @@ export class SettingsV2Component implements OnInit {
);
constructor(
private readonly vaultNudgesService: VaultNudgesService,
private readonly nudgesService: NudgesService,
private readonly accountService: AccountService,
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
private readonly configService: ConfigService,
@@ -94,10 +94,10 @@ export class SettingsV2Component implements OnInit {
);
}
async dismissBadge(type: VaultNudgeType) {
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) {
async dismissBadge(type: NudgeType) {
if (await firstValueFrom(this.showVaultBadge$)) {
const account = await firstValueFrom(this.authenticatedAccount$);
await this.vaultNudgesService.dismissNudge(type, account.id as UserId, true);
await this.nudgesService.dismissNudge(type, account.id as UserId, true);
}
}
}

View File

@@ -1,6 +1,6 @@
import { inject } from "@angular/core";
import { CanActivateFn } from "@angular/router";
import { switchMap, tap } from "rxjs";
import { CanActivateFn, Router } from "@angular/router";
import { map, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -13,18 +13,22 @@ export const canAccessAtRiskPasswords: CanActivateFn = () => {
const taskService = inject(TaskService);
const toastService = inject(ToastService);
const i18nService = inject(I18nService);
const router = inject(Router);
return accountService.activeAccount$.pipe(
filterOutNullish(),
switchMap((user) => taskService.tasksEnabled$(user.id)),
tap((tasksEnabled) => {
map((tasksEnabled) => {
if (!tasksEnabled) {
toastService.showToast({
variant: "error",
title: "",
message: i18nService.t("accessDenied"),
message: i18nService.t("noPermissionsViewPage"),
});
return router.createUrlTree(["/tabs/vault"]);
}
return true;
}),
);
};

View File

@@ -11,7 +11,6 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -66,11 +65,7 @@ export class AssignCollections {
route.queryParams.pipe(
switchMap(async ({ cipherId }) => {
const cipherDomain = await this.cipherService.get(cipherId, userId);
const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption(
cipherDomain,
userId,
);
return cipherDomain.decrypt(key);
return await this.cipherService.decrypt(cipherDomain, userId);
}),
),
),

View File

@@ -81,6 +81,7 @@ describe("OpenAttachmentsComponent", () => {
useValue: {
get: getCipher,
getKeyForCipherKeyDecryption: () => Promise.resolve(null),
decrypt: jest.fn().mockResolvedValue(cipherView),
},
},
{

View File

@@ -81,9 +81,7 @@ export class OpenAttachmentsComponent implements OnInit {
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
const cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
const cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
if (!cipher.organizationId) {
this.cipherIsAPartOfFreeOrg = false;

View File

@@ -40,12 +40,9 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
'copyFieldValue' | i18n: singleCopiableLogin.key : singleCopiableLogin.value
"
[appCopyClick]="singleCopiableLogin.value"
[valueLabel]="singleCopiableLogin.key"
showToast
[appA11yTitle]="singleCopiableLogin.key"
[appCopyField]="$any(singleCopiableLogin.field)"
[cipher]="cipher"
></button>
<ng-container *ngIf="!singleCopiableLogin">
<button

View File

@@ -15,6 +15,7 @@ import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy
type CipherItem = {
value: string;
key: string;
field?: string;
};
@Component({
@@ -43,15 +44,23 @@ export class ItemCopyActionsComponent {
);
}
/*
* singleCopiableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
* code to be copied correctly. See #14167
*/
get singleCopiableLogin() {
const loginItems: CipherItem[] = [
{ value: this.cipher.login.username, key: "username" },
{ value: this.cipher.login.password, key: "password" },
{ value: this.cipher.login.totp, key: "totp" },
{ value: this.cipher.login.username, key: "copyUsername", field: "username" },
{ value: this.cipher.login.password, key: "copyPassword", field: "password" },
{ value: this.cipher.login.totp, key: "copyVerificationCode", field: "totp" },
];
// If both the password and username are visible but the password is hidden, return the username
if (!this.cipher.viewPassword && this.cipher.login.username && this.cipher.login.password) {
return { value: this.cipher.login.username, key: this.i18nService.t("username") };
return {
value: this.cipher.login.username,
key: this.i18nService.t("copyUsername"),
field: "username",
};
}
return this.findSingleCopiableItem(loginItems);
}
@@ -78,12 +87,10 @@ export class ItemCopyActionsComponent {
* Given a list of CipherItems, if there is only one item with a value,
* return it with the translated key. Otherwise return null
*/
findSingleCopiableItem(items: { value: string; key: string }[]): CipherItem | null {
const singleItemWithValue = items.find(
(key) => key.value && items.every((f) => f === key || !f.value),
);
return singleItemWithValue
? { value: singleItemWithValue.value, key: this.i18nService.t(singleItemWithValue.key) }
findSingleCopiableItem(items: CipherItem[]): CipherItem | null {
const itemsWithValue = items.filter(({ value }) => !!value);
return itemsWithValue.length === 1
? { ...itemsWithValue[0], key: this.i18nService.t(itemsWithValue[0].key) }
: null;
}

View File

@@ -1,29 +0,0 @@
<ng-container *ngIf="showNewCustomizationSettingsCallout">
<button
type="button"
class="tw-absolute tw-bottom-[12px] tw-right-[47px]"
[bitPopoverTriggerFor]="newCustomizationOptionsCallout"
[position]="'above-end'"
[popoverOpen]="true"
#triggerRef="popoverTrigger"
></button>
<bit-popover
[title]="'newCustomizationOptionsCalloutTitle' | i18n"
#newCustomizationOptionsCallout
(closed)="dismissCallout()"
>
<div bitTypography="body2" (click)="goToAppearance()">
{{ "newCustomizationOptionsCalloutContent" | i18n }}
<a
tabIndex="0"
bitLink
class="tw-font-bold"
linkType="primary"
routerLink="/appearance"
(keydown.enter)="goToAppearance()"
>
{{ "newCustomizationOptionsCalloutLink" | i18n }}
</a>
</div>
</bit-popover>
</ng-container>

View File

@@ -1,81 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { ButtonModule, PopoverModule } from "@bitwarden/components";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
import { VaultPageService } from "../vault-page.service";
@Component({
selector: "new-settings-callout",
templateUrl: "new-settings-callout.component.html",
standalone: true,
imports: [PopoverModule, JslibModule, CommonModule, ButtonModule],
providers: [VaultPageService],
})
export class NewSettingsCalloutComponent implements OnInit, OnDestroy {
protected showNewCustomizationSettingsCallout = false;
protected activeUserId: UserId | null = null;
constructor(
private accountService: AccountService,
private vaultProfileService: VaultProfileService,
private vaultPageService: VaultPageService,
private router: Router,
private logService: LogService,
private copyButtonService: VaultPopupCopyButtonsService,
private vaultSettingsService: VaultSettingsService,
) {}
async ngOnInit() {
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const showQuickCopyActions = await firstValueFrom(this.copyButtonService.showQuickCopyActions$);
const clickItemsToAutofillVaultView = await firstValueFrom(
this.vaultSettingsService.clickItemsToAutofillVaultView$,
);
let profileCreatedDate: Date;
try {
profileCreatedDate = await this.vaultProfileService.getProfileCreationDate(this.activeUserId);
} catch (e) {
this.logService.error("Error getting profile creation date", e);
// Default to before the cutoff date to ensure the callout is shown
profileCreatedDate = new Date("2024-12-24");
}
const hasCalloutBeenDismissed = await firstValueFrom(
this.vaultPageService.isCalloutDismissed(this.activeUserId),
);
this.showNewCustomizationSettingsCallout =
!showQuickCopyActions &&
!clickItemsToAutofillVaultView &&
!hasCalloutBeenDismissed &&
profileCreatedDate < new Date("2024-12-25");
}
async goToAppearance() {
await this.router.navigate(["/appearance"]);
}
async dismissCallout() {
if (this.activeUserId) {
await this.vaultPageService.dismissCallout(this.activeUserId);
}
}
async ngOnDestroy() {
await this.dismissCallout();
}
}

View File

@@ -1,35 +0,0 @@
import { inject, Injectable } from "@angular/core";
import { map, Observable } from "rxjs";
import {
BANNERS_DISMISSED_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
export const NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY = new UserKeyDefinition<boolean>(
BANNERS_DISMISSED_DISK,
"newCustomizationOptionsCalloutDismissed",
{
deserializer: (calloutDismissed) => calloutDismissed,
clearOn: [], // Do not clear dismissed callouts
},
);
@Injectable()
export class VaultPageService {
private stateProvider = inject(StateProvider);
isCalloutDismissed(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.state$.pipe(map((dismissed) => !!dismissed));
}
async dismissCallout(userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.update(() => true);
}
}

View File

@@ -69,8 +69,6 @@ export class PasswordHistoryV2Component implements OnInit {
const activeUserId = activeAccount.id as UserId;
const cipher = await this.cipherService.get(cipherId, activeUserId);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
}
}

View File

@@ -36,7 +36,7 @@
[subtitle]="'emptyVaultNudgeBody' | i18n"
[buttonText]="'emptyVaultNudgeButton' | i18n"
(onButtonClick)="navigateToImport()"
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.EmptyVaultNudge)"
(onDismiss)="dismissVaultNudgeSpotlight(NudgeType.EmptyVaultNudge)"
>
</bit-spotlight>
</ng-container>
@@ -44,9 +44,13 @@
<div class="tw-mb-4" *ngIf="showHasItemsVaultSpotlight$ | async">
<bit-spotlight
[title]="'hasItemsVaultNudgeTitle' | i18n"
[subtitle]="'hasItemsVaultNudgeBody' | i18n"
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.HasVaultItems)"
(onDismiss)="dismissVaultNudgeSpotlight(NudgeType.HasVaultItems)"
>
<ul class="tw-pl-4 tw-text-main" bitTypography="body2">
<li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyTwo" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyThree" | i18n }}</li>
</ul>
</bit-spotlight>
</div>
<vault-at-risk-password-callout
@@ -103,5 +107,4 @@
></app-vault-list-items-container>
</div>
</ng-container>
<new-settings-callout></new-settings-callout>
</popup-page>

View File

@@ -19,16 +19,23 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
import {
ButtonModule,
DialogService,
Icons,
NoItemsModule,
TypographyModule,
} from "@bitwarden/components";
import {
DecryptionFailureDialogComponent,
NudgesService,
NudgeType,
SpotlightComponent,
VaultIcons,
VaultNudgesService,
VaultNudgeType,
} from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
@@ -49,9 +56,7 @@ import {
NewItemDropdownV2Component,
NewItemInitialValues,
} from "./new-item-dropdown/new-item-dropdown-v2.component";
import { NewSettingsCalloutComponent } from "./new-settings-callout/new-settings-callout.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { VaultPageService } from "./vault-page.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
@@ -83,27 +88,24 @@ enum VaultState {
ScrollingModule,
VaultHeaderV2Component,
AtRiskPasswordCalloutComponent,
NewSettingsCalloutComponent,
SpotlightComponent,
RouterModule,
TypographyModule,
],
providers: [VaultPageService],
})
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
VaultNudgeType = VaultNudgeType;
NudgeType = NudgeType;
cipherType = CipherType;
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
showEmptyVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
switchMap((userId) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, userId),
this.nudgesService.showNudgeSpotlight$(NudgeType.EmptyVaultNudge, userId),
),
map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed),
);
showHasItemsVaultSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
switchMap((userId) => this.vaultNudgesService.showNudge$(VaultNudgeType.HasVaultItems, userId)),
map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed),
switchMap((userId) => this.nudgesService.showNudgeSpotlight$(NudgeType.HasVaultItems, userId)),
);
activeUserId: UserId | null = null;
@@ -144,7 +146,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
protected noResultsIcon = Icons.NoResults;
protected VaultStateEnum = VaultState;
protected showNewCustomizationSettingsCallout = false;
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
@@ -156,8 +157,9 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private dialogService: DialogService,
private vaultCopyButtonsService: VaultPopupCopyButtonsService,
private introCarouselService: IntroCarouselService,
private vaultNudgesService: VaultNudgesService,
private nudgesService: NudgesService,
private router: Router,
private i18nService: I18nService,
) {
combineLatest([
this.vaultPopupItemsService.emptyVault$,
@@ -225,8 +227,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
}
}
async dismissVaultNudgeSpotlight(type: VaultNudgeType) {
await this.vaultNudgesService.dismissNudge(type, this.activeUserId as UserId);
async dismissVaultNudgeSpotlight(type: NudgeType) {
await this.nudgesService.dismissNudge(type, this.activeUserId as UserId);
}
protected readonly FeatureFlag = FeatureFlag;

View File

@@ -82,6 +82,7 @@ describe("ViewV2Component", () => {
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
deleteWithServer: jest.fn().mockResolvedValue(undefined),
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
decrypt: jest.fn().mockResolvedValue(mockCipher),
};
beforeEach(async () => {

View File

@@ -203,9 +203,7 @@ export class ViewV2Component {
async getCipherData(id: string, userId: UserId) {
const cipher = await this.cipherService.get(id, userId);
return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
);
return await this.cipherService.decrypt(cipher, userId);
}
async editCipher() {

View File

@@ -7,7 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CardComponent, LinkModule, TypographyModule } from "@bitwarden/components";
import { VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
import { NudgesService, NudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
@@ -32,12 +32,12 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
})
export class DownloadBitwardenComponent implements OnInit {
constructor(
private vaultNudgeService: VaultNudgesService,
private nudgesService: NudgesService,
private accountService: AccountService,
) {}
async ngOnInit() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.vaultNudgeService.dismissNudge(VaultNudgeType.DownloadBitwarden, userId);
await this.nudgesService.dismissNudge(NudgeType.DownloadBitwarden, userId);
}
}

View File

@@ -71,6 +71,7 @@
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.40.0",
"form-data": "4.0.1",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",

View File

@@ -59,15 +59,11 @@ export class ShareCommand {
return Response.badRequest("This item already belongs to an organization.");
}
const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const cipherView = await this.cipherService.decrypt(cipher, activeUserId);
try {
await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId);
const updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {

View File

@@ -106,6 +106,8 @@ export class LoginCommand {
return Response.badRequest("client_secret is required.");
}
} else if (options.sso != null && this.canInteract) {
// If the optional Org SSO Identifier isn't provided, the option value is `true`.
const orgSsoIdentifier = options.sso === true ? null : options.sso;
const passwordOptions: any = {
type: "password",
length: 64,
@@ -119,7 +121,7 @@ export class LoginCommand {
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
try {
const ssoParams = await this.openSsoPrompt(codeChallenge, state);
const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier);
ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier;
} catch {
@@ -664,6 +666,7 @@ export class LoginCommand {
private async openSsoPrompt(
codeChallenge: string,
state: string,
orgSsoIdentifier: string,
): Promise<{ ssoCode: string; orgIdentifier: string }> {
const env = await firstValueFrom(this.environmentService.environment$);
@@ -712,6 +715,8 @@ export class LoginCommand {
this.ssoRedirectUri,
state,
codeChallenge,
null,
orgSsoIdentifier,
);
this.platformUtilsService.launchUri(webAppSsoUrl);
});

View File

@@ -90,9 +90,7 @@ export class EditCommand {
return Response.notFound();
}
let cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
let cipherView = await this.cipherService.decrypt(cipher, activeUserId);
if (cipherView.isDeleted) {
return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
}
@@ -100,9 +98,7 @@ export class EditCommand {
const encCipher = await this.cipherService.encrypt(cipherView, activeUserId);
try {
const updatedCipher = await this.cipherService.updateWithServer(encCipher);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
@@ -132,12 +128,7 @@ export class EditCommand {
cipher,
activeUserId,
);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(
updatedCipher,
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {

View File

@@ -116,9 +116,7 @@ export class GetCommand extends DownloadCommand {
if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id, activeUserId);
if (cipher != null) {
decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
decCipher = await this.cipherService.decrypt(cipher, activeUserId);
}
} else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted(activeUserId);

View File

@@ -118,7 +118,10 @@ export class Program extends BaseProgram {
.description("Log into a user account.")
.option("--method <method>", "Two-step login method.")
.option("--code <code>", "Two-step login code.")
.option("--sso", "Log in with Single-Sign On.")
.option(
"--sso [identifier]",
"Log in with Single-Sign On with optional organization identifier.",
)
.option("--apikey", "Log in with an Api Key.")
.option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option(

View File

@@ -139,12 +139,14 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
@@ -284,6 +286,7 @@ export class ServiceContainer {
ssoUrlService: SsoUrlService;
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
bulkEncryptService: FallbackBulkEncryptService;
cipherEncryptionService: CipherEncryptionService;
constructor() {
let p = null;
@@ -679,6 +682,11 @@ export class ServiceContainer {
this.accountService,
);
this.cipherEncryptionService = new DefaultCipherEncryptionService(
this.sdkService,
this.logService,
);
this.cipherService = new CipherService(
this.keyService,
this.domainSettingsService,
@@ -694,6 +702,7 @@ export class ServiceContainer {
this.stateProvider,
this.accountService,
this.logService,
this.cipherEncryptionService,
);
this.folderService = new FolderService(

View File

@@ -93,9 +93,7 @@ export class CreateCommand {
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try {
const newCipher = await this.cipherService.createWithServer(cipher);
const decCipher = await newCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(newCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(newCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
@@ -162,9 +160,7 @@ export class CreateCommand {
new Uint8Array(fileBuf).buffer,
activeUserId,
);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
return Response.success(new CipherResponse(decCipher));
} catch (e) {
return Response.error(e);

View File

@@ -3045,9 +3045,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.43.1"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
@@ -3479,9 +3479,9 @@ dependencies = [
[[package]]
name = "widestring"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "winapi"
@@ -3568,7 +3568,7 @@ dependencies = [
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
"windows-strings",
]
[[package]]
@@ -3643,13 +3643,13 @@ dependencies = [
[[package]]
name = "windows-registry"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
dependencies = [
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
"windows-strings",
]
[[package]]
@@ -3670,15 +3670,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
@@ -3730,29 +3721,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -3765,12 +3740,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -3783,12 +3752,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -3801,24 +3764,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -3831,12 +3782,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_plugin_authenticator"
version = "0.0.0"
@@ -3858,12 +3803,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -3876,12 +3815,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -3894,12 +3827,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.3"

View File

@@ -50,17 +50,17 @@ simplelog = "=0.12.2"
ssh-encoding = "=0.2.0"
ssh-key = {version = "=0.6.7", default-features = false }
sysinfo = "0.35.0"
thiserror = "=1.0.69"
tokio = "=1.43.1"
thiserror = "=2.0.12"
tokio = "=1.45.0"
tokio-stream = "=0.1.15"
tokio-util = "=0.7.13"
typenum = "=1.18.0"
uniffi = "=0.28.3"
widestring = "=1.1.0"
widestring = "=1.2.0"
windows = "=0.61.1"
windows-core = "=0.61.0"
windows-future = "=0.2.0"
windows-registry = "=0.4.0"
windows-registry = "=0.5.1"
zbus = "=4.4.0"
zbus_polkit = "=4.0.0"
zeroizing-alloc = "=0.1.0"

View File

@@ -243,7 +243,7 @@
},
"snap": {
"summary": "Bitwarden is a secure and free password manager for all of your devices.",
"description": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.",
"description": "Password Manager\n**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.",
"autoStart": true,
"base": "core22",
"confinement": "strict",

View File

@@ -54,6 +54,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
@Component({
selector: "app-settings",
templateUrl: "settings.component.html",
standalone: false,
})
export class SettingsComponent implements OnInit, OnDestroy {
// For use in template

View File

@@ -18,5 +18,6 @@ import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@b
useExisting: VaultTimeoutInputComponent,
},
],
standalone: false,
})
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {}

View File

@@ -11,7 +11,16 @@ import {
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router";
import { filter, firstValueFrom, map, Subject, switchMap, takeUntil, timeout } from "rxjs";
import {
filter,
firstValueFrom,
lastValueFrom,
map,
Subject,
switchMap,
takeUntil,
timeout,
} from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
@@ -56,11 +65,11 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { DialogRef, DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
import { SettingsComponent } from "./accounts/settings.component";
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
@@ -78,7 +87,6 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
<ng-template #settings></ng-template>
<ng-template #premium></ng-template>
<ng-template #passwordHistory></ng-template>
<ng-template #appFolderAddEdit></ng-template>
<ng-template #exportVault></ng-template>
<ng-template #appGenerator></ng-template>
<ng-template #loginApproval></ng-template>
@@ -93,6 +101,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
<bit-toast-container></bit-toast-container>
`,
standalone: false,
})
export class AppComponent implements OnInit, OnDestroy {
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@@ -101,8 +110,6 @@ export class AppComponent implements OnInit, OnDestroy {
passwordHistoryRef: ViewContainerRef;
@ViewChild("exportVault", { read: ViewContainerRef, static: true })
exportVaultModalRef: ViewContainerRef;
@ViewChild("appFolderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
generatorModalRef: ViewContainerRef;
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
@@ -464,25 +471,11 @@ export class AppComponent implements OnInit, OnDestroy {
async addFolder() {
this.modalService.closeAll();
const [modal, childComponent] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => (comp.folderId = null),
);
this.modal = modal;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
childComponent.onSavedFolder.subscribe(async () => {
this.modal.close();
// 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.syncService.fullSync(false);
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService);
const result = await lastValueFrom(dialogRef.closed);
if (result === AddEditFolderDialogResult.Created) {
await this.syncService.fullSync(false);
}
}
async openGenerator() {

View File

@@ -8,6 +8,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
@Component({
selector: "app-avatar",
template: `<img *ngIf="src" [src]="src" [ngClass]="{ 'rounded-circle': circle }" />`,
standalone: false,
})
export class AvatarComponent implements OnChanges, OnInit {
@Input() size = 45;

View File

@@ -54,6 +54,7 @@ type InactiveAccount = ActiveAccount & {
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
standalone: false,
})
export class AccountSwitcherComponent implements OnInit {
activeAccount$: Observable<ActiveAccount | null>;

View File

@@ -3,5 +3,6 @@ import { Component } from "@angular/core";
@Component({
selector: "app-header",
templateUrl: "header.component.html",
standalone: false,
})
export class HeaderComponent {}

View File

@@ -11,6 +11,7 @@ import { SearchBarService, SearchBarState } from "./search-bar.service";
@Component({
selector: "app-search",
templateUrl: "search.component.html",
standalone: false,
})
export class SearchComponent implements OnInit, OnDestroy {
state: SearchBarState;

View File

@@ -28,6 +28,7 @@ const BroadcasterSubscriptionId = "SetPasswordComponent";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
standalone: false,
})
export class SetPasswordComponent extends BaseSetPasswordComponent implements OnInit, OnDestroy {
constructor(

View File

@@ -5,5 +5,6 @@ import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
standalone: false,
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}

View File

@@ -199,9 +199,7 @@ export class DesktopAutofillService implements OnDestroy {
return;
}
const decrypted = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const decrypted = await this.cipherService.decrypt(cipher, activeUserId);
const fido2Credential = decrypted.login.fido2Credentials?.[0];
if (!fido2Credential) {

View File

@@ -13,6 +13,7 @@ import { DialogService, ToastService } from "@bitwarden/components";
@Component({
selector: "app-premium",
templateUrl: "premium.component.html",
standalone: false,
})
export class PremiumComponent extends BasePremiumComponent {
constructor(

View File

@@ -5,5 +5,6 @@ import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitward
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
standalone: false,
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

Some files were not shown because too many files have changed in this diff Show More