mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
Merge branch 'main' into km/decrypt-obj
This commit is contained in:
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -157,6 +157,7 @@
|
||||
"html-webpack-injector",
|
||||
"html-webpack-plugin",
|
||||
"interprocess",
|
||||
"itertools",
|
||||
"json5",
|
||||
"keytar",
|
||||
"libc",
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/auto-branch-updater.yml
vendored
2
.github/workflows/auto-branch-updater.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: echo "branch=${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: 'eu-web-${{ steps.setup.outputs.branch }}'
|
||||
fetch-depth: 0
|
||||
|
||||
12
.github/workflows/build-browser.yml
vendored
12
.github/workflows/build-browser.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
working-directory: apps/browser
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
artifact_name: "dist-opera-MV3"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -542,7 +542,7 @@ jobs:
|
||||
- build-safari
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
8
.github/workflows/build-cli.yml
vendored
8
.github/workflows/build-cli.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
_WIN_PKG_VERSION: 3.5
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -520,7 +520,7 @@ jobs:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
34
.github/workflows/build-desktop.yml
vendored
34
.github/workflows/build-desktop.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: true
|
||||
@@ -173,24 +173,14 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Free disk space for build
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /usr/share/miniconda
|
||||
sudo rm -rf /usr/share/az_*
|
||||
sudo rm -rf /usr/local/julia*
|
||||
sudo rm -rf /usr/lib/mono
|
||||
sudo rm -rf /usr/lib/heroku
|
||||
sudo rm -rf /usr/local/aws-cli
|
||||
sudo rm -rf /usr/local/aws-sam-cli
|
||||
- name: Free disk space
|
||||
uses: bitwarden/gh-actions/free-disk-space@main
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
@@ -343,7 +333,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -491,7 +481,7 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -759,7 +749,7 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1004,7 +994,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1244,7 +1234,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1519,7 +1509,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1860,7 +1850,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
8
.github/workflows/build-web.yml
vendored
8
.github/workflows/build-web.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
echo "server_ref=$SERVER_REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out Server repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
path: server
|
||||
repository: bitwarden/server
|
||||
@@ -367,7 +367,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
permission-pull-requests: write # for generating pull requests
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/lint-crowdin-config.yml
vendored
2
.github/workflows/lint-crowdin-config.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
- name: Install cargo-deny
|
||||
uses: taiki-e/install-action@073d46cba2cde38f6698c798566c1b3e24feeb44 # v2.62.67
|
||||
with:
|
||||
tool: cargo-deny@0.18.5
|
||||
tool: cargo-deny@0.18.6
|
||||
|
||||
- name: Run cargo deny
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
|
||||
4
.github/workflows/locales-lint.yml
vendored
4
.github/workflows/locales-lint.yml
vendored
@@ -17,11 +17,11 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Checkout base branch repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
|
||||
2
.github/workflows/nx.yml
vendored
2
.github/workflows/nx.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/publish-cli.yml
vendored
6
.github/workflows/publish-cli.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
6
.github/workflows/publish-desktop.yml
vendored
6
.github/workflows/publish-desktop.yml
vendored
@@ -204,7 +204,7 @@ jobs:
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/publish-web.yml
vendored
4
.github/workflows/publish-web.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "Github Release Option: $_RELEASE_OPTION"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/release-browser.yml
vendored
4
.github/workflows/release-browser.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/release-cli.yml
vendored
2
.github/workflows/release-cli.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/release-desktop.yml
vendored
2
.github/workflows/release-desktop.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
release_channel: ${{ steps.release_channel.outputs.channel }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/release-web.yml
vendored
2
.github/workflows/release-web.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
tag_version: ${{ steps.version.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/repository-management.yml
vendored
4
.github/workflows/repository-management.yml
vendored
@@ -105,7 +105,7 @@ jobs:
|
||||
permission-contents: write # for committing and pushing to current branch
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
@@ -471,7 +471,7 @@ jobs:
|
||||
permission-contents: write # for creating and pushing new branch
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
2
.github/workflows/review-code.yml
vendored
2
.github/workflows/review-code.yml
vendored
@@ -2,7 +2,7 @@ name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
types: [opened, labeled]
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Check out clients repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
sudo apt-get install -y gnome-keyring dbus-x11
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
- rust-coverage
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/version-auto-bump.yml
vendored
2
.github/workflows/version-auto-bump.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
permission-contents: write # for committing and pushing to the current branch
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:bit": "npm run build:bit:chrome",
|
||||
|
||||
@@ -2498,7 +2498,7 @@
|
||||
}
|
||||
},
|
||||
"topLayerHijackWarning": {
|
||||
"message": "This page is interfering with the Bitwarden experience. The Bitwarden inline menu has been temporarily disabled as a safety measure."
|
||||
"message": "Bu səhifə Bitwarden təcrübəsinə müdaxilə edir. Bitwarden daxili menyusu, təhlükəsizlik tədbiri olaraq müvəqqəti sıradan çıxarılıb."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Ana parolu ayarla"
|
||||
@@ -4124,7 +4124,7 @@
|
||||
"message": "Avto-doldurula bilmir"
|
||||
},
|
||||
"cannotAutofillExactMatch": {
|
||||
"message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item."
|
||||
"message": "İlkin uyuşma 'Tam Uyuşur' olaraq ayarlanıb. Hazırkı veb sayt, bu element üçün saxlanılmış giriş məlumatları ilə tam uyuşmur."
|
||||
},
|
||||
"okay": {
|
||||
"message": "Oldu"
|
||||
|
||||
@@ -6039,5 +6039,8 @@
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1486,7 +1486,7 @@
|
||||
"message": "选择一个文件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "项目已传输"
|
||||
"message": "项目已转移"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"message": "文件最大为 500 MB。"
|
||||
@@ -3804,7 +3804,7 @@
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
"message": "桌面",
|
||||
"message": "桌面端",
|
||||
"description": "Desktop app"
|
||||
},
|
||||
"webVault": {
|
||||
@@ -5707,7 +5707,7 @@
|
||||
"message": "导入现有密码"
|
||||
},
|
||||
"emptyVaultNudgeBody": {
|
||||
"message": "使用导入器快速将登录传输到 Bitwarden 而无需手动添加。"
|
||||
"message": "使用导入器快速将登录转移到 Bitwarden 而无需手动添加。"
|
||||
},
|
||||
"emptyVaultNudgeButton": {
|
||||
"message": "立即导入"
|
||||
@@ -6014,7 +6014,7 @@
|
||||
"message": "我该如何管理我的密码库?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "传输项目到 $ORGANIZATION$",
|
||||
"message": "转移项目到 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6023,7 +6023,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6032,7 +6032,7 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "接受传输"
|
||||
"message": "接受转移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "拒绝并退出"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Message, MessageTypes } from "./message";
|
||||
|
||||
const SENDER = "bitwarden-webauthn";
|
||||
@@ -25,7 +23,7 @@ type Handler = (
|
||||
* handling aborts and exceptions across separate execution contexts.
|
||||
*/
|
||||
export class Messenger {
|
||||
private messageEventListener: (event: MessageEvent<MessageWithMetadata>) => void | null = null;
|
||||
private messageEventListener: ((event: MessageEvent<MessageWithMetadata>) => void) | null = null;
|
||||
private onDestroy = new EventTarget();
|
||||
|
||||
/**
|
||||
@@ -60,6 +58,12 @@ export class Messenger {
|
||||
this.broadcastChannel.addEventListener(this.messageEventListener);
|
||||
}
|
||||
|
||||
private stripMetadata({ SENDER, senderId, ...message }: MessageWithMetadata): Message {
|
||||
void SENDER;
|
||||
void senderId;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the content script and returns the response.
|
||||
* AbortController signals will be forwarded to the content script.
|
||||
@@ -74,7 +78,9 @@ export class Messenger {
|
||||
|
||||
try {
|
||||
const promise = new Promise<Message>((resolve) => {
|
||||
localPort.onmessage = (event: MessageEvent<MessageWithMetadata>) => resolve(event.data);
|
||||
localPort.onmessage = (event: MessageEvent<MessageWithMetadata>) => {
|
||||
resolve(this.stripMetadata(event.data));
|
||||
};
|
||||
});
|
||||
|
||||
const abortListener = () =>
|
||||
@@ -129,7 +135,9 @@ export class Messenger {
|
||||
|
||||
try {
|
||||
const handlerResponse = await this.handler(message, abortController);
|
||||
port.postMessage({ ...handlerResponse, SENDER });
|
||||
if (handlerResponse !== undefined) {
|
||||
port.postMessage({ ...handlerResponse, SENDER });
|
||||
}
|
||||
} catch (error) {
|
||||
port.postMessage({
|
||||
SENDER,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
@@ -15,14 +13,17 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
private readonly setElementStyles = setElementStyles;
|
||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||
private port: chrome.runtime.Port | null = null;
|
||||
private portKey: string;
|
||||
private portKey?: string;
|
||||
private readonly extensionOrigin: string;
|
||||
private iframeMutationObserver: MutationObserver;
|
||||
private iframe: HTMLIFrameElement;
|
||||
private ariaAlertElement: HTMLDivElement;
|
||||
private ariaAlertTimeout: number | NodeJS.Timeout;
|
||||
private delayedCloseTimeout: number | NodeJS.Timeout;
|
||||
private fadeInTimeout: number | NodeJS.Timeout;
|
||||
/**
|
||||
* Initialized in initMenuIframe which makes it safe to assert non null by lifecycle.
|
||||
*/
|
||||
private iframe!: HTMLIFrameElement;
|
||||
private ariaAlertElement?: HTMLDivElement;
|
||||
private ariaAlertTimeout: number | NodeJS.Timeout | null = null;
|
||||
private delayedCloseTimeout: number | NodeJS.Timeout | null = null;
|
||||
private fadeInTimeout: number | NodeJS.Timeout | null = null;
|
||||
private readonly fadeInOpacityTransition = "opacity 125ms ease-out 0s";
|
||||
private readonly fadeOutOpacityTransition = "opacity 65ms ease-out 0s";
|
||||
private iframeStyles: Partial<CSSStyleDeclaration> = {
|
||||
@@ -50,7 +51,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
};
|
||||
private foreignMutationsCount = 0;
|
||||
private mutationObserverIterations = 0;
|
||||
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
|
||||
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout | null = null;
|
||||
private readonly backgroundPortMessageHandlers: BackgroundPortMessageHandlers = {
|
||||
initAutofillInlineMenuButton: ({ message }) => this.initAutofillInlineMenu(message),
|
||||
initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenu(message),
|
||||
@@ -134,7 +135,9 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
this.port.onDisconnect.addListener(this.handlePortDisconnect);
|
||||
this.port.onMessage.addListener(this.handlePortMessage);
|
||||
|
||||
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||
if (this.ariaAlert) {
|
||||
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -155,7 +158,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
|
||||
this.ariaAlertTimeout = globalThis.setTimeout(async () => {
|
||||
const isFieldFocused = await this.sendExtensionMessage("checkIsFieldCurrentlyFocused");
|
||||
if (isFieldFocused || triggeredByUser) {
|
||||
if ((isFieldFocused || triggeredByUser) && this.ariaAlertElement) {
|
||||
this.shadow.appendChild(this.ariaAlertElement);
|
||||
}
|
||||
this.ariaAlertTimeout = null;
|
||||
@@ -242,7 +245,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
*/
|
||||
private initAutofillInlineMenuList(message: AutofillInlineMenuIframeExtensionMessage) {
|
||||
const { theme } = message;
|
||||
let borderColor: string;
|
||||
let borderColor: string | undefined;
|
||||
let verifiedTheme = theme;
|
||||
if (verifiedTheme === ThemeTypes.System) {
|
||||
verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
@@ -274,8 +277,8 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
*
|
||||
* @param position - The position styles to apply to the iframe
|
||||
*/
|
||||
private updateIframePosition(position: Partial<CSSStyleDeclaration>) {
|
||||
if (!globalThis.document.hasFocus()) {
|
||||
private updateIframePosition(position?: Partial<CSSStyleDeclaration>) {
|
||||
if (!position || !globalThis.document.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -295,7 +298,9 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
this.handleFadeInInlineMenuIframe();
|
||||
}
|
||||
|
||||
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||
if (this.ariaAlert) {
|
||||
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,8 +364,8 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
* @param customElement - The element to update the styles for
|
||||
* @param styles - The styles to apply to the element
|
||||
*/
|
||||
private updateElementStyles(customElement: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
||||
if (!customElement) {
|
||||
private updateElementStyles(customElement: HTMLElement, styles?: Partial<CSSStyleDeclaration>) {
|
||||
if (!customElement || !styles) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
@@ -84,11 +82,15 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
}
|
||||
const { type, typeData, params } = message.data;
|
||||
|
||||
if (!typeData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentNotificationBarType && type !== this.currentNotificationBarType) {
|
||||
this.closeNotificationBar();
|
||||
}
|
||||
|
||||
const initData = {
|
||||
const initData: NotificationBarIframeInitData = {
|
||||
type: type as NotificationType,
|
||||
isVaultLocked: typeData.isVaultLocked,
|
||||
theme: typeData.theme,
|
||||
@@ -116,7 +118,9 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
const closedByUser =
|
||||
typeof message.data?.closedByUser === "boolean" ? message.data.closedByUser : true;
|
||||
if (message.data?.fadeOutNotification) {
|
||||
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
|
||||
if (this.notificationBarIframeElement) {
|
||||
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
|
||||
}
|
||||
globalThis.setTimeout(() => this.closeNotificationBar(closedByUser), 150);
|
||||
return;
|
||||
}
|
||||
@@ -166,7 +170,9 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
this.createNotificationBarElement();
|
||||
|
||||
this.setupInitNotificationBarMessageListener(initData);
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
if (this.notificationBarRootElement) {
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +185,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
const isNotificationFresh =
|
||||
initData.launchTimestamp && Date.now() - initData.launchTimestamp < 250;
|
||||
|
||||
this.currentNotificationBarType = initData.type;
|
||||
this.currentNotificationBarType = initData.type ?? null;
|
||||
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
||||
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
||||
const parentOrigin = globalThis.location.origin;
|
||||
@@ -206,11 +212,13 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
* This will animate the notification bar into view.
|
||||
*/
|
||||
private handleNotificationBarIframeOnLoad = () => {
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{ transform: "translateX(0)", opacity: "1" },
|
||||
true,
|
||||
);
|
||||
if (this.notificationBarIframeElement) {
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{ transform: "translateX(0)", opacity: "1" },
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
this.notificationBarIframeElement?.removeEventListener(
|
||||
EVENTS.LOAD,
|
||||
@@ -252,6 +260,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
const handleInitNotificationBarMessage = (event: MessageEvent) => {
|
||||
const { source, data } = event;
|
||||
if (
|
||||
!this.notificationBarIframeElement?.contentWindow ||
|
||||
source !== this.notificationBarIframeElement.contentWindow ||
|
||||
data?.command !== "initNotificationBar"
|
||||
) {
|
||||
@@ -282,13 +291,14 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
return;
|
||||
}
|
||||
|
||||
this.notificationBarIframeElement.remove();
|
||||
this.notificationBarIframeElement?.remove();
|
||||
this.notificationBarIframeElement = null;
|
||||
|
||||
this.notificationBarElement.remove();
|
||||
this.notificationBarElement?.remove();
|
||||
this.notificationBarElement = null;
|
||||
this.notificationBarShadowRoot = null;
|
||||
this.notificationBarRootElement.remove();
|
||||
|
||||
this.notificationBarRootElement?.remove();
|
||||
this.notificationBarRootElement = null;
|
||||
|
||||
const removableNotificationTypes = new Set([
|
||||
@@ -297,7 +307,11 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
NotificationTypes.AtRiskPassword,
|
||||
] as NotificationType[]);
|
||||
|
||||
if (closedByUserAction && removableNotificationTypes.has(this.currentNotificationBarType)) {
|
||||
if (
|
||||
closedByUserAction &&
|
||||
this.currentNotificationBarType &&
|
||||
removableNotificationTypes.has(this.currentNotificationBarType)
|
||||
) {
|
||||
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
|
||||
}
|
||||
|
||||
@@ -310,7 +324,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
* @param message - The message to send to the notification bar iframe.
|
||||
*/
|
||||
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
||||
if (this.notificationBarIframeElement) {
|
||||
if (this.notificationBarIframeElement?.contentWindow) {
|
||||
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
</popup-header>
|
||||
|
||||
<div class="tw-bg-background-alt">
|
||||
<div *ngIf="!defaultBrowserAutofillDisabled && (showSpotlightNudge$ | async)" class="tw-mb-6">
|
||||
<bit-spotlight
|
||||
[title]="'autofillSpotlightTitle' | i18n"
|
||||
[subtitle]="'autofillSpotlightDesc' | i18n"
|
||||
[buttonText]="spotlightButtonText"
|
||||
(onDismiss)="dismissSpotlight()"
|
||||
(onButtonClick)="disableBrowserAutofillSettingsFromNudge($event)"
|
||||
[buttonIcon]="spotlightButtonIcon"
|
||||
></bit-spotlight>
|
||||
</div>
|
||||
@if (showSpotlightNudge$ | async) {
|
||||
<div class="tw-mb-6">
|
||||
<bit-spotlight
|
||||
[title]="'autofillSpotlightTitle' | i18n"
|
||||
[subtitle]="'autofillSpotlightDesc' | i18n"
|
||||
[buttonText]="spotlightButtonText"
|
||||
(onDismiss)="dismissSpotlight()"
|
||||
(onButtonClick)="disableBrowserAutofillSettingsFromNudge($event)"
|
||||
[buttonIcon]="spotlightButtonIcon"
|
||||
></bit-spotlight>
|
||||
</div>
|
||||
}
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "autofillSuggestionsSectionTitle" | i18n }}</h2>
|
||||
|
||||
@@ -611,6 +611,10 @@ export class AutofillComponent implements OnInit {
|
||||
if (this.canOverrideBrowserAutofillSetting) {
|
||||
this.defaultBrowserAutofillDisabled = true;
|
||||
await this.updateDefaultBrowserAutofillDisabled();
|
||||
await this.nudgesService.dismissNudge(
|
||||
NudgeType.AutofillNudge,
|
||||
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
|
||||
);
|
||||
} else {
|
||||
await this.openURI(event, this.disablePasswordManagerURI);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,15 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
"button",
|
||||
"image",
|
||||
"file",
|
||||
"search",
|
||||
"url",
|
||||
"date",
|
||||
"time",
|
||||
"datetime", // Note: datetime is deprecated in HTML5; keeping here for backwards compatibility
|
||||
"datetime-local",
|
||||
"week",
|
||||
"color",
|
||||
"range",
|
||||
]);
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -126,6 +126,7 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -164,6 +165,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
|
||||
@@ -463,6 +465,7 @@ export default class MainBackground {
|
||||
themeStateService: DefaultThemeStateService;
|
||||
autoSubmitLoginBackground: AutoSubmitLoginBackground;
|
||||
sdkService: SdkService;
|
||||
registerSdkService: RegisterSdkService;
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
endUserNotificationService: EndUserNotificationService;
|
||||
@@ -578,7 +581,7 @@ export default class MainBackground {
|
||||
"ephemeral",
|
||||
"bitwarden-ephemeral",
|
||||
);
|
||||
await sessionStorage.save("session-key", derivedKey);
|
||||
await sessionStorage.save("session-key", derivedKey.toJSON());
|
||||
return derivedKey;
|
||||
});
|
||||
|
||||
@@ -797,18 +800,6 @@ export default class MainBackground {
|
||||
this.apiService,
|
||||
this.accountService,
|
||||
);
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
@@ -846,6 +837,37 @@ export default class MainBackground {
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.registerSdkService = new DefaultRegisterSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
this.registerSdkService,
|
||||
this.securityStateService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.pinService = new PinService(
|
||||
this.encryptService,
|
||||
this.logService,
|
||||
@@ -1013,9 +1035,7 @@ export default class MainBackground {
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
|
||||
this.providerService = new ProviderService(this.stateProvider);
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.syncService = new DefaultSyncService(
|
||||
this.masterPasswordService,
|
||||
this.accountService,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
export type PhishingResource = {
|
||||
name?: string;
|
||||
remoteUrl: string;
|
||||
checksumUrl: string;
|
||||
todayUrl: string;
|
||||
/** Matcher used to decide whether a given URL matches an entry from this resource */
|
||||
match: (url: URL, entry: string) => boolean;
|
||||
};
|
||||
|
||||
export const PhishingResourceType = Object.freeze({
|
||||
Domains: "domains",
|
||||
Links: "links",
|
||||
} as const);
|
||||
|
||||
export type PhishingResourceType = (typeof PhishingResourceType)[keyof typeof PhishingResourceType];
|
||||
|
||||
export const PHISHING_RESOURCES: Record<PhishingResourceType, PhishingResource[]> = {
|
||||
[PhishingResourceType.Domains]: [
|
||||
{
|
||||
name: "Phishing.Database Domains",
|
||||
remoteUrl:
|
||||
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/master/phishing-domains-ACTIVE.txt",
|
||||
checksumUrl:
|
||||
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.md5",
|
||||
todayUrl:
|
||||
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/refs/heads/master/phishing-domains-NEW-today.txt",
|
||||
match: (url: URL, entry: string) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const candidate = entry.trim().toLowerCase().replace(/\/$/, "");
|
||||
// If entry contains a scheme, strip it for comparison
|
||||
const e = candidate.replace(/^https?:\/\//, "");
|
||||
// Compare against hostname or host+path
|
||||
if (e === url.hostname.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
const urlNoProto = url.href
|
||||
.toLowerCase()
|
||||
.replace(/https?:\/\//, "")
|
||||
.replace(/\/$/, "");
|
||||
return urlNoProto === e || urlNoProto.startsWith(e + "/");
|
||||
},
|
||||
},
|
||||
],
|
||||
[PhishingResourceType.Links]: [
|
||||
{
|
||||
name: "Phishing.Database Links",
|
||||
remoteUrl:
|
||||
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/master/phishing-links-ACTIVE.txt",
|
||||
checksumUrl:
|
||||
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-links-ACTIVE.txt.md5",
|
||||
todayUrl:
|
||||
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/refs/heads/master/phishing-links-NEW-today.txt",
|
||||
match: (url: URL, entry: string) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
// Basic HTML entity decode for common cases (the lists sometimes contain &)
|
||||
const decodeHtml = (s: string) => s.replace(/&/g, "&");
|
||||
|
||||
const normalizedEntry = decodeHtml(entry.trim()).toLowerCase().replace(/\/$/, "");
|
||||
|
||||
// Normalize URL for comparison - always strip protocol for consistent matching
|
||||
const normalizedUrl = decodeHtml(url.href).toLowerCase().replace(/\/$/, "");
|
||||
const urlNoProto = normalizedUrl.replace(/^https?:\/\//, "");
|
||||
|
||||
// Strip protocol from entry if present (http:// and https:// should be treated as equivalent)
|
||||
const entryNoProto = normalizedEntry.replace(/^https?:\/\//, "");
|
||||
|
||||
// Compare full path (without protocol) - exact match
|
||||
if (urlNoProto === entryNoProto) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if URL starts with entry (prefix match for subpaths/query/hash)
|
||||
// e.g., entry "site.com/phish" matches "site.com/phish/subpage" or "site.com/phish?id=1"
|
||||
if (
|
||||
urlNoProto.startsWith(entryNoProto + "/") ||
|
||||
urlNoProto.startsWith(entryNoProto + "?") ||
|
||||
urlNoProto.startsWith(entryNoProto + "#")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function getPhishingResources(
|
||||
type: PhishingResourceType,
|
||||
index = 0,
|
||||
): PhishingResource | undefined {
|
||||
const list = PHISHING_RESOURCES[type] ?? [];
|
||||
return list[index];
|
||||
}
|
||||
@@ -25,7 +25,7 @@ describe("PhishingDataService", () => {
|
||||
};
|
||||
|
||||
let fetchChecksumSpy: jest.SpyInstance;
|
||||
let fetchDomainsSpy: jest.SpyInstance;
|
||||
let fetchWebAddressesSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
@@ -45,113 +45,113 @@ describe("PhishingDataService", () => {
|
||||
platformUtilsService,
|
||||
);
|
||||
|
||||
fetchChecksumSpy = jest.spyOn(service as any, "fetchPhishingDomainsChecksum");
|
||||
fetchDomainsSpy = jest.spyOn(service as any, "fetchPhishingDomains");
|
||||
fetchChecksumSpy = jest.spyOn(service as any, "fetchPhishingChecksum");
|
||||
fetchWebAddressesSpy = jest.spyOn(service as any, "fetchPhishingWebAddresses");
|
||||
});
|
||||
|
||||
describe("isPhishingDomains", () => {
|
||||
it("should detect a phishing domain", async () => {
|
||||
describe("isPhishingWebAddress", () => {
|
||||
it("should detect a phishing web address", async () => {
|
||||
setMockState({
|
||||
domains: ["phish.com", "badguy.net"],
|
||||
webAddresses: ["phish.com", "badguy.net"],
|
||||
timestamp: Date.now(),
|
||||
checksum: "abc123",
|
||||
applicationVersion: "1.0.0",
|
||||
});
|
||||
const url = new URL("http://phish.com");
|
||||
const result = await service.isPhishingDomain(url);
|
||||
const result = await service.isPhishingWebAddress(url);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should not detect a safe domain", async () => {
|
||||
it("should not detect a safe web address", async () => {
|
||||
setMockState({
|
||||
domains: ["phish.com", "badguy.net"],
|
||||
webAddresses: ["phish.com", "badguy.net"],
|
||||
timestamp: Date.now(),
|
||||
checksum: "abc123",
|
||||
applicationVersion: "1.0.0",
|
||||
});
|
||||
const url = new URL("http://safe.com");
|
||||
const result = await service.isPhishingDomain(url);
|
||||
const result = await service.isPhishingWebAddress(url);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should match against root domain", async () => {
|
||||
it("should match against root web address", async () => {
|
||||
setMockState({
|
||||
domains: ["phish.com", "badguy.net"],
|
||||
webAddresses: ["phish.com", "badguy.net"],
|
||||
timestamp: Date.now(),
|
||||
checksum: "abc123",
|
||||
applicationVersion: "1.0.0",
|
||||
});
|
||||
const url = new URL("http://phish.com/about");
|
||||
const result = await service.isPhishingDomain(url);
|
||||
const result = await service.isPhishingWebAddress(url);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should not error on empty state", async () => {
|
||||
setMockState(undefined as any);
|
||||
const url = new URL("http://phish.com/about");
|
||||
const result = await service.isPhishingDomain(url);
|
||||
const result = await service.isPhishingWebAddress(url);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNextDomains", () => {
|
||||
it("refetches all domains if applicationVersion has changed", async () => {
|
||||
describe("getNextWebAddresses", () => {
|
||||
it("refetches all web addresses if applicationVersion has changed", async () => {
|
||||
const prev: PhishingData = {
|
||||
domains: ["a.com"],
|
||||
webAddresses: ["a.com"],
|
||||
timestamp: Date.now() - 60000,
|
||||
checksum: "old",
|
||||
applicationVersion: "1.0.0",
|
||||
};
|
||||
fetchChecksumSpy.mockResolvedValue("new");
|
||||
fetchDomainsSpy.mockResolvedValue(["d.com", "e.com"]);
|
||||
fetchWebAddressesSpy.mockResolvedValue(["d.com", "e.com"]);
|
||||
platformUtilsService.getApplicationVersion.mockResolvedValue("2.0.0");
|
||||
|
||||
const result = await service.getNextDomains(prev);
|
||||
const result = await service.getNextWebAddresses(prev);
|
||||
|
||||
expect(result!.domains).toEqual(["d.com", "e.com"]);
|
||||
expect(result!.webAddresses).toEqual(["d.com", "e.com"]);
|
||||
expect(result!.checksum).toBe("new");
|
||||
expect(result!.applicationVersion).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("only updates timestamp if checksum matches", async () => {
|
||||
const prev: PhishingData = {
|
||||
domains: ["a.com"],
|
||||
webAddresses: ["a.com"],
|
||||
timestamp: Date.now() - 60000,
|
||||
checksum: "abc",
|
||||
applicationVersion: "1.0.0",
|
||||
};
|
||||
fetchChecksumSpy.mockResolvedValue("abc");
|
||||
const result = await service.getNextDomains(prev);
|
||||
expect(result!.domains).toEqual(prev.domains);
|
||||
const result = await service.getNextWebAddresses(prev);
|
||||
expect(result!.webAddresses).toEqual(prev.webAddresses);
|
||||
expect(result!.checksum).toBe("abc");
|
||||
expect(result!.timestamp).not.toBe(prev.timestamp);
|
||||
});
|
||||
|
||||
it("patches daily domains if cache is fresh", async () => {
|
||||
const prev: PhishingData = {
|
||||
domains: ["a.com"],
|
||||
webAddresses: ["a.com"],
|
||||
timestamp: Date.now() - 60000,
|
||||
checksum: "old",
|
||||
applicationVersion: "1.0.0",
|
||||
};
|
||||
fetchChecksumSpy.mockResolvedValue("new");
|
||||
fetchDomainsSpy.mockResolvedValue(["b.com", "c.com"]);
|
||||
const result = await service.getNextDomains(prev);
|
||||
expect(result!.domains).toEqual(["a.com", "b.com", "c.com"]);
|
||||
fetchWebAddressesSpy.mockResolvedValue(["b.com", "c.com"]);
|
||||
const result = await service.getNextWebAddresses(prev);
|
||||
expect(result!.webAddresses).toEqual(["a.com", "b.com", "c.com"]);
|
||||
expect(result!.checksum).toBe("new");
|
||||
});
|
||||
|
||||
it("fetches all domains if cache is old", async () => {
|
||||
const prev: PhishingData = {
|
||||
domains: ["a.com"],
|
||||
webAddresses: ["a.com"],
|
||||
timestamp: Date.now() - 2 * 24 * 60 * 60 * 1000,
|
||||
checksum: "old",
|
||||
applicationVersion: "1.0.0",
|
||||
};
|
||||
fetchChecksumSpy.mockResolvedValue("new");
|
||||
fetchDomainsSpy.mockResolvedValue(["d.com", "e.com"]);
|
||||
const result = await service.getNextDomains(prev);
|
||||
expect(result!.domains).toEqual(["d.com", "e.com"]);
|
||||
fetchWebAddressesSpy.mockResolvedValue(["d.com", "e.com"]);
|
||||
const result = await service.getNextWebAddresses(prev);
|
||||
expect(result!.webAddresses).toEqual(["d.com", "e.com"]);
|
||||
expect(result!.checksum).toBe("new");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,14 +20,16 @@ import { ScheduledTaskNames, TaskSchedulerService } from "@bitwarden/common/plat
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { GlobalStateProvider, KeyDefinition, PHISHING_DETECTION_DISK } from "@bitwarden/state";
|
||||
|
||||
import { getPhishingResources, PhishingResourceType } from "../phishing-resources";
|
||||
|
||||
export type PhishingData = {
|
||||
domains: string[];
|
||||
webAddresses: string[];
|
||||
timestamp: number;
|
||||
checksum: string;
|
||||
|
||||
/**
|
||||
* We store the application version to refetch the entire dataset on a new client release.
|
||||
* This counteracts daily appends updates not removing inactive or false positive domains.
|
||||
* This counteracts daily appends updates not removing inactive or false positive web addresses.
|
||||
*/
|
||||
applicationVersion: string;
|
||||
};
|
||||
@@ -37,34 +39,27 @@ export const PHISHING_DOMAINS_KEY = new KeyDefinition<PhishingData>(
|
||||
"phishingDomains",
|
||||
{
|
||||
deserializer: (value: PhishingData) =>
|
||||
value ?? { domains: [], timestamp: 0, checksum: "", applicationVersion: "" },
|
||||
value ?? { webAddresses: [], timestamp: 0, checksum: "", applicationVersion: "" },
|
||||
},
|
||||
);
|
||||
|
||||
/** Coordinates fetching, caching, and patching of known phishing domains */
|
||||
/** Coordinates fetching, caching, and patching of known phishing web addresses */
|
||||
export class PhishingDataService {
|
||||
private static readonly RemotePhishingDatabaseUrl =
|
||||
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/master/phishing-domains-ACTIVE.txt";
|
||||
private static readonly RemotePhishingDatabaseChecksumUrl =
|
||||
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.md5";
|
||||
private static readonly RemotePhishingDatabaseTodayUrl =
|
||||
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/refs/heads/master/phishing-domains-NEW-today.txt";
|
||||
|
||||
private _testDomains = this.getTestDomains();
|
||||
private _testWebAddresses = this.getTestWebAddresses();
|
||||
private _cachedState = this.globalStateProvider.get(PHISHING_DOMAINS_KEY);
|
||||
private _domains$ = this._cachedState.state$.pipe(
|
||||
private _webAddresses$ = this._cachedState.state$.pipe(
|
||||
map(
|
||||
(state) =>
|
||||
new Set(
|
||||
(state?.domains?.filter((line) => line.trim().length > 0) ?? []).concat(
|
||||
this._testDomains,
|
||||
(state?.webAddresses?.filter((line) => line.trim().length > 0) ?? []).concat(
|
||||
this._testWebAddresses,
|
||||
"phishing.testcategory.com", // Included for QA to test in prod
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// How often are new domains added to the remote?
|
||||
// How often are new web addresses added to the remote?
|
||||
readonly UPDATE_INTERVAL_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
private _triggerUpdate$ = new Subject<void>();
|
||||
@@ -75,7 +70,7 @@ export class PhishingDataService {
|
||||
this._cachedState.state$.pipe(
|
||||
first(), // Only take the first value to avoid an infinite loop when updating the cache below
|
||||
switchMap(async (cachedState) => {
|
||||
const next = await this.getNextDomains(cachedState);
|
||||
const next = await this.getNextWebAddresses(cachedState);
|
||||
if (next) {
|
||||
await this._cachedState.update(() => next);
|
||||
this.logService.info(`[PhishingDataService] cache updated`);
|
||||
@@ -85,7 +80,7 @@ export class PhishingDataService {
|
||||
count: 3,
|
||||
delay: (err, count) => {
|
||||
this.logService.error(
|
||||
`[PhishingDataService] Unable to update domains. Attempt ${count}.`,
|
||||
`[PhishingDataService] Unable to update web addresses. Attempt ${count}.`,
|
||||
err,
|
||||
);
|
||||
return timer(5 * 60 * 1000); // 5 minutes
|
||||
@@ -97,7 +92,7 @@ export class PhishingDataService {
|
||||
err: unknown /** Eslint actually crashed if you remove this type: https://github.com/cartant/eslint-plugin-rxjs/issues/122 */,
|
||||
) => {
|
||||
this.logService.error(
|
||||
"[PhishingDataService] Retries unsuccessful. Unable to update domains.",
|
||||
"[PhishingDataService] Retries unsuccessful. Unable to update web addresses.",
|
||||
err,
|
||||
);
|
||||
return EMPTY;
|
||||
@@ -114,6 +109,7 @@ export class PhishingDataService {
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private logService: LogService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private resourceType: PhishingResourceType = PhishingResourceType.Links,
|
||||
) {
|
||||
this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.phishingDomainUpdate, () => {
|
||||
this._triggerUpdate$.next();
|
||||
@@ -125,22 +121,31 @@ export class PhishingDataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given URL is a known phishing domain
|
||||
* Checks if the given URL is a known phishing web address
|
||||
*
|
||||
* @param url The URL to check
|
||||
* @returns True if the URL is a known phishing domain, false otherwise
|
||||
* @returns True if the URL is a known phishing web address, false otherwise
|
||||
*/
|
||||
async isPhishingDomain(url: URL): Promise<boolean> {
|
||||
const domains = await firstValueFrom(this._domains$);
|
||||
const result = domains.has(url.hostname);
|
||||
if (result) {
|
||||
return true;
|
||||
async isPhishingWebAddress(url: URL): Promise<boolean> {
|
||||
// Use domain (hostname) matching for domain resources, and link matching for links resources
|
||||
const entries = await firstValueFrom(this._webAddresses$);
|
||||
|
||||
const resource = getPhishingResources(this.resourceType);
|
||||
if (resource && resource.match) {
|
||||
for (const entry of entries) {
|
||||
if (resource.match(url, entry)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Default/domain behavior: exact hostname match as a fallback
|
||||
return entries.has(url.hostname);
|
||||
}
|
||||
|
||||
async getNextDomains(prev: PhishingData | null): Promise<PhishingData | null> {
|
||||
prev = prev ?? { domains: [], timestamp: 0, checksum: "", applicationVersion: "" };
|
||||
async getNextWebAddresses(prev: PhishingData | null): Promise<PhishingData | null> {
|
||||
prev = prev ?? { webAddresses: [], timestamp: 0, checksum: "", applicationVersion: "" };
|
||||
const timestamp = Date.now();
|
||||
const prevAge = timestamp - prev.timestamp;
|
||||
this.logService.info(`[PhishingDataService] Cache age: ${prevAge}`);
|
||||
@@ -148,7 +153,7 @@ export class PhishingDataService {
|
||||
const applicationVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
|
||||
// If checksum matches, return existing data with new timestamp & version
|
||||
const remoteChecksum = await this.fetchPhishingDomainsChecksum();
|
||||
const remoteChecksum = await this.fetchPhishingChecksum(this.resourceType);
|
||||
if (remoteChecksum && prev.checksum === remoteChecksum) {
|
||||
this.logService.info(
|
||||
`[PhishingDataService] Remote checksum matches local checksum, updating timestamp only.`,
|
||||
@@ -157,66 +162,66 @@ export class PhishingDataService {
|
||||
}
|
||||
// Checksum is different, data needs to be updated.
|
||||
|
||||
// Approach 1: Fetch only new domains and append
|
||||
// Approach 1: Fetch only new web addresses and append
|
||||
const isOneDayOldMax = prevAge <= this.UPDATE_INTERVAL_DURATION;
|
||||
if (isOneDayOldMax && applicationVersion === prev.applicationVersion) {
|
||||
const dailyDomains: string[] = await this.fetchPhishingDomains(
|
||||
PhishingDataService.RemotePhishingDatabaseTodayUrl,
|
||||
);
|
||||
const webAddressesTodayUrl = getPhishingResources(this.resourceType)!.todayUrl;
|
||||
const dailyWebAddresses: string[] =
|
||||
await this.fetchPhishingWebAddresses(webAddressesTodayUrl);
|
||||
this.logService.info(
|
||||
`[PhishingDataService] ${dailyDomains.length} new phishing domains added`,
|
||||
`[PhishingDataService] ${dailyWebAddresses.length} new phishing web addresses added`,
|
||||
);
|
||||
return {
|
||||
domains: prev.domains.concat(dailyDomains),
|
||||
webAddresses: prev.webAddresses.concat(dailyWebAddresses),
|
||||
checksum: remoteChecksum,
|
||||
timestamp,
|
||||
applicationVersion,
|
||||
};
|
||||
}
|
||||
|
||||
// Approach 2: Fetch all domains
|
||||
const domains = await this.fetchPhishingDomains(PhishingDataService.RemotePhishingDatabaseUrl);
|
||||
// Approach 2: Fetch all web addresses
|
||||
const remoteUrl = getPhishingResources(this.resourceType)!.remoteUrl;
|
||||
const remoteWebAddresses = await this.fetchPhishingWebAddresses(remoteUrl);
|
||||
return {
|
||||
domains,
|
||||
webAddresses: remoteWebAddresses,
|
||||
timestamp,
|
||||
checksum: remoteChecksum,
|
||||
applicationVersion,
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchPhishingDomainsChecksum() {
|
||||
const response = await this.apiService.nativeFetch(
|
||||
new Request(PhishingDataService.RemotePhishingDatabaseChecksumUrl),
|
||||
);
|
||||
private async fetchPhishingChecksum(type: PhishingResourceType = PhishingResourceType.Domains) {
|
||||
const checksumUrl = getPhishingResources(type)!.checksumUrl;
|
||||
const response = await this.apiService.nativeFetch(new Request(checksumUrl));
|
||||
if (!response.ok) {
|
||||
throw new Error(`[PhishingDataService] Failed to fetch checksum: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
private async fetchPhishingDomains(url: string) {
|
||||
private async fetchPhishingWebAddresses(url: string) {
|
||||
const response = await this.apiService.nativeFetch(new Request(url));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`[PhishingDataService] Failed to fetch domains: ${response.status}`);
|
||||
throw new Error(`[PhishingDataService] Failed to fetch web addresses: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.text().then((text) => text.split("\n"));
|
||||
}
|
||||
|
||||
private getTestDomains() {
|
||||
private getTestWebAddresses() {
|
||||
const flag = devFlagEnabled("testPhishingUrls");
|
||||
if (!flag) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const domains = devFlagValue("testPhishingUrls") as unknown[];
|
||||
if (domains && domains instanceof Array) {
|
||||
const webAddresses = devFlagValue("testPhishingUrls") as unknown[];
|
||||
if (webAddresses && webAddresses instanceof Array) {
|
||||
this.logService.debug(
|
||||
"[PhishingDetectionService] Dev flag enabled for testing phishing detection. Adding test phishing domains:",
|
||||
domains,
|
||||
"[PhishingDetectionService] Dev flag enabled for testing phishing detection. Adding test phishing web addresses:",
|
||||
webAddresses,
|
||||
);
|
||||
return domains as string[];
|
||||
return webAddresses as string[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export class PhishingDetectionService {
|
||||
this._ignoredHostnames.delete(url.hostname);
|
||||
return;
|
||||
}
|
||||
const isPhishing = await phishingDataService.isPhishingDomain(url);
|
||||
const isPhishing = await phishingDataService.isPhishingWebAddress(url);
|
||||
if (!isPhishing) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
WINDOW,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AUTOFILL_NUDGE_SERVICE } from "@bitwarden/angular/vault";
|
||||
import { SingleNudgeService } from "@bitwarden/angular/vault/services/default-single-nudge.service";
|
||||
import {
|
||||
LoginComponentService,
|
||||
TwoFactorAuthComponentService,
|
||||
@@ -208,6 +210,7 @@ import {
|
||||
} from "../../platform/system-notifications/browser-system-notification.service";
|
||||
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
||||
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
||||
import { BrowserAutofillNudgeService } from "../../vault/popup/services/browser-autofill-nudge.service";
|
||||
import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
|
||||
@@ -756,6 +759,11 @@ const safeProviders: SafeProvider[] = [
|
||||
MessagingServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AUTOFILL_NUDGE_SERVICE as SafeInjectionToken<SingleNudgeService>,
|
||||
useClass: BrowserAutofillNudgeService,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2015-2025 Bitwarden Inc. All rights reserved.</string>
|
||||
<string>Copyright © 2015-2026 Bitwarden Inc. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2015-2025 Bitwarden Inc. All rights reserved.</string>
|
||||
<string>Copyright © 2015-2026 Bitwarden Inc. All rights reserved.</string>
|
||||
<key>NSHumanReadableDescription</key>
|
||||
<string>A secure and free password manager for all of your devices.</string>
|
||||
<key>SFSafariAppExtensionBundleIdentifiersToReplace</key>
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
<main class="tw-top-0">
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="goToEditSend.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="goToEditSend.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
{{ formatExpirationDate() }}
|
||||
</p>
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<div
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
</main>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
{{ formatExpirationDate() }}
|
||||
</p>
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
|
||||
@@ -34,13 +34,11 @@
|
||||
<i slot="start" class="bwi bwi-check-circle" aria-hidden="true"></i>
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<p class="tw-pr-2">{{ "autofill" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="!(isBrowserAutofillSettingOverridden$ | async) && (showAutofillBadge$ | async)"
|
||||
bitBadge
|
||||
variant="notification"
|
||||
[attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||
>1</span
|
||||
>
|
||||
@if (showAutofillBadge$ | async) {
|
||||
<span bitBadge variant="notification" [attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||
>1</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -148,31 +148,7 @@ describe("SettingsV2Component", () => {
|
||||
expect(openSpy).toHaveBeenCalledWith(dialogService);
|
||||
});
|
||||
|
||||
it("isBrowserAutofillSettingOverridden$ emits the value from the AutofillBrowserSettingsService", async () => {
|
||||
pushActiveAccount();
|
||||
|
||||
mockAutofillSettings.isBrowserAutofillSettingOverridden.mockResolvedValue(true);
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const value = await firstValueFrom(component["isBrowserAutofillSettingOverridden$"]);
|
||||
expect(value).toBe(true);
|
||||
|
||||
mockAutofillSettings.isBrowserAutofillSettingOverridden.mockResolvedValue(false);
|
||||
|
||||
const fixture2 = TestBed.createComponent(SettingsV2Component);
|
||||
const component2 = fixture2.componentInstance;
|
||||
fixture2.detectChanges();
|
||||
await fixture2.whenStable();
|
||||
|
||||
const value2 = await firstValueFrom(component2["isBrowserAutofillSettingOverridden$"]);
|
||||
expect(value2).toBe(false);
|
||||
});
|
||||
|
||||
it("showAutofillBadge$ emits true when default autofill is NOT disabled and nudge is true", async () => {
|
||||
it("showAutofillBadge$ emits true when showNudgeBadge is true", async () => {
|
||||
pushActiveAccount();
|
||||
|
||||
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) =>
|
||||
@@ -184,30 +160,10 @@ describe("SettingsV2Component", () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
mockAutofillSettings.defaultBrowserAutofillDisabled$.next(false);
|
||||
|
||||
const value = await firstValueFrom(component.showAutofillBadge$);
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
|
||||
it("showAutofillBadge$ emits false when default autofill IS disabled even if nudge is true", async () => {
|
||||
pushActiveAccount();
|
||||
|
||||
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) =>
|
||||
of(type === NudgeType.AutofillNudge),
|
||||
);
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
mockAutofillSettings.defaultBrowserAutofillDisabled$.next(true);
|
||||
|
||||
const value = await firstValueFrom(component.showAutofillBadge$);
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
|
||||
it("dismissBadge dismisses when showVaultBadge$ emits true", async () => {
|
||||
const acct = pushActiveAccount();
|
||||
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import { filter, firstValueFrom, Observable, shareReplay, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -28,8 +19,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
@@ -55,12 +44,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
export class SettingsV2Component {
|
||||
NudgeType = NudgeType;
|
||||
|
||||
protected isBrowserAutofillSettingOverridden$ = from(
|
||||
this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
|
||||
BrowserApi.getBrowserClientVendor(window),
|
||||
),
|
||||
);
|
||||
|
||||
private authenticatedAccount$: Observable<Account> = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => account !== null),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
@@ -82,23 +65,13 @@ export class SettingsV2Component {
|
||||
),
|
||||
);
|
||||
|
||||
showAutofillBadge$: Observable<boolean> = combineLatest([
|
||||
this.autofillBrowserSettingsService.defaultBrowserAutofillDisabled$,
|
||||
this.authenticatedAccount$,
|
||||
]).pipe(
|
||||
switchMap(([defaultBrowserAutofillDisabled, account]) =>
|
||||
this.nudgesService.showNudgeBadge$(NudgeType.AutofillNudge, account.id).pipe(
|
||||
map((badgeStatus) => {
|
||||
return !defaultBrowserAutofillDisabled && badgeStatus;
|
||||
}),
|
||||
),
|
||||
),
|
||||
showAutofillBadge$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) => this.nudgesService.showNudgeBadge$(NudgeType.AutofillNudge, account.id)),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly nudgesService: NudgesService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||
private readonly accountProfileStateService: BillingAccountProfileStateService,
|
||||
private readonly dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
}
|
||||
@if (savedUrls().length > 1) {
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mt-4 tw-mb-1 tw-pt-2">
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-medium">
|
||||
{{ "savedWebsites" | i18n: savedUrls().length }}
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-medium tw-mb-0">
|
||||
{{ "savedWebsites" | i18n: savedUrls().length.toString() }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,17 +13,6 @@
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
<!-- Autofill confirmation handles both 'autofill' and 'autofill and save' so no need to show both -->
|
||||
@if (!(autofillConfirmationFlagEnabled$ | async)) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*ngIf="canEdit && isLogin"
|
||||
(click)="doAutofillAndSave()"
|
||||
>
|
||||
{{ "fillAndSave" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
UriMatchStrategy,
|
||||
UriMatchStrategySetting,
|
||||
} from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -40,10 +39,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
openSimpleDialog: jest.fn().mockResolvedValue(true),
|
||||
open: jest.fn(),
|
||||
};
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const configService = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation(() => featureFlag$.asObservable()),
|
||||
};
|
||||
const cipherService = {
|
||||
getFullCipherView: jest.fn(),
|
||||
encrypt: jest.fn(),
|
||||
@@ -93,7 +88,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ItemMoreOptionsComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: CipherService, useValue: cipherService },
|
||||
{ provide: VaultPopupAutofillService, useValue: autofillSvc },
|
||||
|
||||
@@ -152,22 +146,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher);
|
||||
});
|
||||
|
||||
it("calls the autofill service to autofill without showing the confirmation dialog when the feature flag is disabled", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(cipherService.getFullCipherView).toHaveBeenCalled();
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1);
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: "cipher-1" }),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if the user fails master password reprompt", async () => {
|
||||
baseCipher.reprompt = 2; // Master Password reprompt enabled
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
@@ -181,7 +159,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
});
|
||||
|
||||
it("does not show the exact match dialog when the default match strategy is Exact and autofill confirmation is not to be shown", async () => {
|
||||
// autofill confirmation dialog is not shown when either the feature flag is disabled
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Exact);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
await component.doAutofill();
|
||||
@@ -191,8 +168,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
|
||||
describe("autofill confirmation dialog", () => {
|
||||
beforeEach(() => {
|
||||
// autofill confirmation dialog is shown when feature flag is enabled
|
||||
featureFlag$.next(true);
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Domain);
|
||||
passwordRepromptService.passwordRepromptCheck.mockResolvedValue(true);
|
||||
});
|
||||
@@ -206,7 +181,7 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher);
|
||||
});
|
||||
|
||||
it("opens the autofill confirmation dialog with filtered saved URLs when the feature flag is enabled", async () => {
|
||||
it("opens the autofill confirmation dialog with filtered saved URLs", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
const openSpy = mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled);
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
@@ -37,7 +35,6 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
import {
|
||||
AutofillConfirmationDialogComponent,
|
||||
@@ -98,10 +95,6 @@ export class ItemMoreOptionsComponent {
|
||||
|
||||
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||
|
||||
protected autofillConfirmationFlagEnabled$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.AutofillConfirmation)
|
||||
.pipe(map((isFeatureFlagEnabled) => isFeatureFlagEnabled));
|
||||
|
||||
protected uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$;
|
||||
|
||||
/**
|
||||
@@ -166,8 +159,6 @@ export class ItemMoreOptionsComponent {
|
||||
private collectionService: CollectionService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private configService: ConfigService,
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
) {}
|
||||
|
||||
@@ -216,13 +207,9 @@ export class ItemMoreOptionsComponent {
|
||||
const cipherHasAllExactMatchLoginUris =
|
||||
uris.length > 0 && uris.every((u) => u.uri && u.match === UriMatchStrategy.Exact);
|
||||
|
||||
const showAutofillConfirmation = await firstValueFrom(this.autofillConfirmationFlagEnabled$);
|
||||
const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$);
|
||||
|
||||
if (
|
||||
showAutofillConfirmation &&
|
||||
(cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact)
|
||||
) {
|
||||
if (cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "cannotAutofill" },
|
||||
content: { key: "cannotAutofillExactMatch" },
|
||||
@@ -233,11 +220,6 @@ export class ItemMoreOptionsComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showAutofillConfirmation) {
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher, true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$);
|
||||
|
||||
if (!currentTab?.url) {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { NudgeStatus, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import { BrowserClientVendors } from "@bitwarden/common/autofill/constants";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../../../libs/common/spec";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
|
||||
import { BrowserAutofillNudgeService } from "./browser-autofill-nudge.service";
|
||||
|
||||
describe("BrowserAutofillNudgeService", () => {
|
||||
let service: BrowserAutofillNudgeService;
|
||||
let vaultProfileService: MockProxy<VaultProfileService>;
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
const userId = "test-user-id" as UserId;
|
||||
const nudgeType = NudgeType.AutofillNudge;
|
||||
|
||||
const notDismissedStatus: NudgeStatus = {
|
||||
hasBadgeDismissed: false,
|
||||
hasSpotlightDismissed: false,
|
||||
};
|
||||
|
||||
const dismissedStatus: NudgeStatus = {
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
};
|
||||
|
||||
// Set profile creation date to now (new account, within 30 days)
|
||||
const recentProfileDate = new Date();
|
||||
|
||||
beforeEach(() => {
|
||||
vaultProfileService = mock<VaultProfileService>();
|
||||
vaultProfileService.getProfileCreationDate.mockResolvedValue(recentProfileDate);
|
||||
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
BrowserAutofillNudgeService,
|
||||
{
|
||||
provide: VaultProfileService,
|
||||
useValue: vaultProfileService,
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(BrowserAutofillNudgeService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("nudgeStatus$", () => {
|
||||
it("returns parent status when browser client is Unknown", async () => {
|
||||
jest
|
||||
.spyOn(BrowserApi, "getBrowserClientVendor")
|
||||
.mockReturnValue(BrowserClientVendors.Unknown);
|
||||
jest.spyOn(BrowserApi, "browserAutofillSettingsOverridden").mockResolvedValue(true);
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(result).toEqual(notDismissedStatus);
|
||||
});
|
||||
|
||||
it("returns parent status when browser autofill is not overridden", async () => {
|
||||
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue(BrowserClientVendors.Chrome);
|
||||
jest.spyOn(BrowserApi, "browserAutofillSettingsOverridden").mockResolvedValue(false);
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(result).toEqual(notDismissedStatus);
|
||||
});
|
||||
|
||||
it("returns dismissed status when browser autofill is overridden", async () => {
|
||||
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue(BrowserClientVendors.Chrome);
|
||||
jest.spyOn(BrowserApi, "browserAutofillSettingsOverridden").mockResolvedValue(true);
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(result).toEqual(dismissedStatus);
|
||||
});
|
||||
|
||||
it("preserves parent dismissed status when account is older than 30 days", async () => {
|
||||
// Set profile creation date to more than 30 days ago
|
||||
const oldProfileDate = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000);
|
||||
vaultProfileService.getProfileCreationDate.mockResolvedValue(oldProfileDate);
|
||||
|
||||
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue(BrowserClientVendors.Chrome);
|
||||
jest.spyOn(BrowserApi, "browserAutofillSettingsOverridden").mockResolvedValue(false);
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(result).toEqual(dismissedStatus);
|
||||
});
|
||||
|
||||
it("combines parent dismissed and browser autofill overridden status", async () => {
|
||||
// Set profile creation date to more than 30 days ago (parent dismisses)
|
||||
const oldProfileDate = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000);
|
||||
vaultProfileService.getProfileCreationDate.mockResolvedValue(oldProfileDate);
|
||||
|
||||
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue(BrowserClientVendors.Chrome);
|
||||
jest.spyOn(BrowserApi, "browserAutofillSettingsOverridden").mockResolvedValue(true);
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(result).toEqual(dismissedStatus);
|
||||
});
|
||||
|
||||
it.each([
|
||||
BrowserClientVendors.Chrome,
|
||||
BrowserClientVendors.Edge,
|
||||
BrowserClientVendors.Opera,
|
||||
BrowserClientVendors.Vivaldi,
|
||||
])("checks browser autofill settings for %s browser", async (browserVendor) => {
|
||||
const getBrowserClientVendorSpy = jest
|
||||
.spyOn(BrowserApi, "getBrowserClientVendor")
|
||||
.mockReturnValue(browserVendor);
|
||||
const browserAutofillSettingsOverriddenSpy = jest
|
||||
.spyOn(BrowserApi, "browserAutofillSettingsOverridden")
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(getBrowserClientVendorSpy).toHaveBeenCalledWith(window);
|
||||
expect(browserAutofillSettingsOverriddenSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not check browser autofill settings for Unknown browser", async () => {
|
||||
jest
|
||||
.spyOn(BrowserApi, "getBrowserClientVendor")
|
||||
.mockReturnValue(BrowserClientVendors.Unknown);
|
||||
const browserAutofillSettingsOverriddenSpy = jest
|
||||
.spyOn(BrowserApi, "browserAutofillSettingsOverridden")
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await firstValueFrom(service.nudgeStatus$(nudgeType, userId));
|
||||
|
||||
expect(browserAutofillSettingsOverriddenSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, switchMap } from "rxjs";
|
||||
|
||||
import { NudgeStatus, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { NewAccountNudgeService } from "@bitwarden/angular/vault/services/custom-nudges-services/new-account-nudge.service";
|
||||
import { BrowserClientVendors } from "@bitwarden/common/autofill/constants";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
|
||||
/**
|
||||
* Browser-specific autofill nudge service.
|
||||
* Extends NewAccountNudgeService (30-day account age check) and adds
|
||||
* browser autofill setting detection.
|
||||
*
|
||||
* Nudge is dismissed if:
|
||||
* - Account is older than 30 days (inherited from NewAccountNudgeService)
|
||||
* - Browser's built-in password manager is already disabled via privacy settings
|
||||
*/
|
||||
@Injectable()
|
||||
export class BrowserAutofillNudgeService extends NewAccountNudgeService {
|
||||
override nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return super.nudgeStatus$(nudgeType, userId).pipe(
|
||||
switchMap(async (status) => {
|
||||
const browserClient = BrowserApi.getBrowserClientVendor(window);
|
||||
const browserAutofillOverridden =
|
||||
browserClient !== BrowserClientVendors.Unknown &&
|
||||
(await BrowserApi.browserAutofillSettingsOverridden());
|
||||
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || browserAutofillOverridden,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || browserAutofillOverridden,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/cli",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -104,6 +104,7 @@ import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { LogLevelType } from "@bitwarden/common/platform/enums";
|
||||
@@ -124,6 +125,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
@@ -323,6 +325,7 @@ export class ServiceContainer {
|
||||
kdfConfigService: KdfConfigService;
|
||||
taskSchedulerService: TaskSchedulerService;
|
||||
sdkService: SdkService;
|
||||
registerSdkService: RegisterSdkService;
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
@@ -632,26 +635,10 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.twoFactorService = new DefaultTwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
this.twoFactorApiService,
|
||||
);
|
||||
|
||||
const sdkClientFactory = flagEnabled("sdk")
|
||||
? new DefaultSdkClientFactory()
|
||||
: new NoopSdkClientFactory();
|
||||
@@ -670,6 +657,41 @@ export class ServiceContainer {
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.registerSdkService = new DefaultRegisterSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
this.registerSdkService,
|
||||
this.securityStateService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.twoFactorService = new DefaultTwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
this.twoFactorApiService,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
@@ -719,10 +741,6 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.loginStrategyService = new LoginStrategyService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<authors>Bitwarden Inc.</authors>
|
||||
<projectUrl>https://bitwarden.com/</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png</iconUrl>
|
||||
<copyright>Copyright © 2015-2025 Bitwarden Inc.</copyright>
|
||||
<copyright>Copyright © 2015-2026 Bitwarden Inc.</copyright>
|
||||
<projectSourceUrl>https://github.com/bitwarden/clients/</projectSourceUrl>
|
||||
<docsUrl>https://help.bitwarden.com/article/cli/</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/bitwarden/clients/issues</bugTrackerUrl>
|
||||
|
||||
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -329,6 +329,7 @@ name = "autotype"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"mockall",
|
||||
"serial_test",
|
||||
"tracing",
|
||||
@@ -1026,6 +1027,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
@@ -1617,6 +1624,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
|
||||
@@ -39,6 +39,7 @@ futures = "=0.3.31"
|
||||
hex = "=0.4.3"
|
||||
homedir = "=0.3.6"
|
||||
interprocess = "=2.2.1"
|
||||
itertools = "=0.14.0"
|
||||
libc = "=0.2.178"
|
||||
linux-keyutils = "=0.2.4"
|
||||
memsec = "=0.7.0"
|
||||
|
||||
@@ -9,6 +9,7 @@ publish.workspace = true
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
itertools.workspace = true
|
||||
mockall = "=0.14.0"
|
||||
serial_test = "=3.2.0"
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -28,6 +28,6 @@ pub fn get_foreground_window_title() -> Result<String> {
|
||||
/// This function returns an `anyhow::Error` if there is any
|
||||
/// issue in typing the input. Detailed reasons will
|
||||
/// vary based on platform implementation.
|
||||
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
|
||||
windowing::type_input(input, keyboard_shortcut)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result<String> {
|
||||
todo!("Bitwarden does not yet support Linux autotype");
|
||||
}
|
||||
|
||||
pub fn type_input(_input: Vec<u16>, _keyboard_shortcut: Vec<String>) -> anyhow::Result<()> {
|
||||
pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
|
||||
todo!("Bitwarden does not yet support Linux autotype");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result<String> {
|
||||
todo!("Bitwarden does not yet support macOS autotype");
|
||||
}
|
||||
|
||||
pub fn type_input(_input: Vec<u16>, _keyboard_shortcut: Vec<String>) -> anyhow::Result<()> {
|
||||
pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
|
||||
todo!("Bitwarden does not yet support macOS autotype");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use tracing::debug;
|
||||
use windows::Win32::Foundation::{GetLastError, SetLastError, WIN32_ERROR};
|
||||
use windows::Win32::{
|
||||
Foundation::{GetLastError, SetLastError, WIN32_ERROR},
|
||||
UI::Input::KeyboardAndMouse::INPUT,
|
||||
};
|
||||
|
||||
mod type_input;
|
||||
mod window_title;
|
||||
@@ -12,7 +16,7 @@ const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
|
||||
/// win32 errors.
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
trait ErrorOperations {
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror>
|
||||
fn set_last_error(err: u32) {
|
||||
debug!(err, "Calling SetLastError");
|
||||
unsafe {
|
||||
@@ -20,7 +24,7 @@ trait ErrorOperations {
|
||||
}
|
||||
}
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror>
|
||||
fn get_last_error() -> WIN32_ERROR {
|
||||
let last_err = unsafe { GetLastError() };
|
||||
debug!("GetLastError(): {}", last_err.to_hresult().message());
|
||||
@@ -36,6 +40,23 @@ pub fn get_foreground_window_title() -> Result<String> {
|
||||
window_title::get_foreground_window_title()
|
||||
}
|
||||
|
||||
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
type_input::type_input(input, keyboard_shortcut)
|
||||
/// `KeyboardShortcutInput` is an `INPUT` of one of the valid shortcut keys:
|
||||
/// - Control
|
||||
/// - Alt
|
||||
/// - Super
|
||||
/// - Shift
|
||||
/// - \[a-z\]\[A-Z\]
|
||||
struct KeyboardShortcutInput(INPUT);
|
||||
|
||||
pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
|
||||
debug!(?keyboard_shortcut, "type_input() called.");
|
||||
|
||||
// convert the raw string input to Windows input and error
|
||||
// if any key is not a valid keyboard shortcut input
|
||||
let keyboard_shortcut: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(s.as_str()))
|
||||
.try_collect()?;
|
||||
|
||||
type_input::type_input(input, &keyboard_shortcut)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
VIRTUAL_KEY,
|
||||
};
|
||||
|
||||
use super::{ErrorOperations, Win32ErrorOperations};
|
||||
use super::{ErrorOperations, KeyboardShortcutInput, Win32ErrorOperations};
|
||||
|
||||
const SHIFT_KEY_STR: &str = "Shift";
|
||||
const CONTROL_KEY_STR: &str = "Control";
|
||||
const ALT_KEY_STR: &str = "Alt";
|
||||
const LEFT_WINDOWS_KEY_STR: &str = "Super";
|
||||
|
||||
const IS_VIRTUAL_KEY: bool = true;
|
||||
const IS_REAL_KEY: bool = false;
|
||||
|
||||
/// `InputOperations` provides an interface to Window32 API for
|
||||
/// working with inputs.
|
||||
@@ -13,7 +21,7 @@ use super::{ErrorOperations, Win32ErrorOperations};
|
||||
trait InputOperations {
|
||||
/// Attempts to type the provided input wherever the user's cursor is.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
fn send_input(inputs: &[INPUT]) -> u32;
|
||||
}
|
||||
|
||||
@@ -21,8 +29,11 @@ struct Win32InputOperations;
|
||||
|
||||
impl InputOperations for Win32InputOperations {
|
||||
fn send_input(inputs: &[INPUT]) -> u32 {
|
||||
const INPUT_STRUCT_SIZE: i32 = std::mem::size_of::<INPUT>() as i32;
|
||||
let insert_count = unsafe { SendInput(inputs, INPUT_STRUCT_SIZE) };
|
||||
const INPUT_STRUCT_SIZE: usize = std::mem::size_of::<INPUT>();
|
||||
|
||||
let size = i32::try_from(INPUT_STRUCT_SIZE).expect("INPUT size to fit in i32");
|
||||
|
||||
let insert_count = unsafe { SendInput(inputs, size) };
|
||||
|
||||
debug!(insert_count, "SendInput() called.");
|
||||
|
||||
@@ -33,40 +44,37 @@ impl InputOperations for Win32InputOperations {
|
||||
/// Attempts to type the input text wherever the user's cursor is.
|
||||
///
|
||||
/// `input` must be a vector of utf-16 encoded characters to insert.
|
||||
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super,
|
||||
/// Shift, letters a - Z
|
||||
/// `keyboard_shortcut` is a vector of valid shortcut keys.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
pub(super) fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
pub(super) fn type_input(input: &[u16], keyboard_shortcut: &[KeyboardShortcutInput]) -> Result<()> {
|
||||
// the length of this vec is always shortcut keys to release + (2x length of input chars)
|
||||
let mut keyboard_inputs: Vec<INPUT> =
|
||||
Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2));
|
||||
|
||||
debug!(?keyboard_shortcut, "Converting keyboard shortcut to input.");
|
||||
|
||||
// Add key "up" inputs for the shortcut
|
||||
for key in keyboard_shortcut {
|
||||
keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?);
|
||||
// insert the keyboard shortcut
|
||||
for shortcut in keyboard_shortcut {
|
||||
keyboard_inputs.push(shortcut.0);
|
||||
}
|
||||
|
||||
add_input(&input, &mut keyboard_inputs);
|
||||
add_input(input, &mut keyboard_inputs);
|
||||
|
||||
send_input::<Win32InputOperations, Win32ErrorOperations>(keyboard_inputs)
|
||||
send_input::<Win32InputOperations, Win32ErrorOperations>(&keyboard_inputs)
|
||||
}
|
||||
|
||||
// Add key "down" and "up" inputs for the input
|
||||
// (currently in this form: {username}/t{password})
|
||||
fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
|
||||
const TAB_KEY: u8 = 9;
|
||||
const TAB_KEY: u16 = 9;
|
||||
|
||||
for i in input {
|
||||
let next_down_input = if *i == TAB_KEY.into() {
|
||||
build_virtual_key_input(InputKeyPress::Down, *i as u8)
|
||||
let next_down_input = if *i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Down, *i)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Down, *i)
|
||||
};
|
||||
let next_up_input = if *i == TAB_KEY.into() {
|
||||
build_virtual_key_input(InputKeyPress::Up, *i as u8)
|
||||
let next_up_input = if *i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Up, *i)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Up, *i)
|
||||
};
|
||||
@@ -76,26 +84,27 @@ fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a valid shortcut key to an "up" keyboard input.
|
||||
///
|
||||
/// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z]
|
||||
fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
||||
const SHIFT_KEY: u8 = 0x10;
|
||||
const SHIFT_KEY_STR: &str = "Shift";
|
||||
const CONTROL_KEY: u8 = 0x11;
|
||||
const CONTROL_KEY_STR: &str = "Control";
|
||||
const ALT_KEY: u8 = 0x12;
|
||||
const ALT_KEY_STR: &str = "Alt";
|
||||
const LEFT_WINDOWS_KEY: u8 = 0x5B;
|
||||
const LEFT_WINDOWS_KEY_STR: &str = "Super";
|
||||
impl TryFrom<&str> for KeyboardShortcutInput {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
Ok(match key.as_str() {
|
||||
SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
|
||||
CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
|
||||
ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
|
||||
LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
|
||||
_ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
|
||||
})
|
||||
fn try_from(key: &str) -> std::result::Result<Self, Self::Error> {
|
||||
const SHIFT_KEY: u16 = 0x10;
|
||||
const CONTROL_KEY: u16 = 0x11;
|
||||
const ALT_KEY: u16 = 0x12;
|
||||
const LEFT_WINDOWS_KEY: u16 = 0x5B;
|
||||
|
||||
// the modifier keys are using the Up keypress variant because the user has already
|
||||
// pressed those keys in order to trigger the feature.
|
||||
let input = match key {
|
||||
SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
|
||||
CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
|
||||
ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
|
||||
LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
|
||||
_ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
|
||||
};
|
||||
|
||||
Ok(KeyboardShortcutInput(input))
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a letter that is a String, get the utf16 encoded
|
||||
@@ -105,7 +114,7 @@ fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
||||
/// Because we only accept [a-z][A-Z], the decimal u16
|
||||
/// cast of the letter is safe because the unicode code point
|
||||
/// of these characters fits in a u16.
|
||||
fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
||||
fn get_alphabetic_hotkey(letter: &str) -> Result<u16> {
|
||||
if letter.len() != 1 {
|
||||
error!(
|
||||
len = letter.len(),
|
||||
@@ -135,23 +144,28 @@ fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
||||
}
|
||||
|
||||
/// An input key can be either pressed (down), or released (up).
|
||||
#[derive(Copy, Clone)]
|
||||
enum InputKeyPress {
|
||||
Down,
|
||||
Up,
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard unicode INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
/// Before modifying this function, make sure you read the `SendInput()` documentation:
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes>
|
||||
fn build_input(key_press: InputKeyPress, character: u16, is_virtual: bool) -> INPUT {
|
||||
let (w_vk, w_scan) = if is_virtual {
|
||||
(VIRTUAL_KEY(character), 0)
|
||||
} else {
|
||||
(VIRTUAL_KEY::default(), character)
|
||||
};
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: Default::default(),
|
||||
wScan: character,
|
||||
wVk: w_vk,
|
||||
wScan: w_scan,
|
||||
dwFlags: KEYEVENTF_UNICODE,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -162,8 +176,8 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: Default::default(),
|
||||
wScan: character,
|
||||
wVk: w_vk,
|
||||
wScan: w_scan,
|
||||
dwFlags: KEYEVENTF_KEYUP | KEYEVENTF_UNICODE,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -173,53 +187,29 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
}
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard virtual-key INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: Default::default(),
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
InputKeyPress::Up => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: KEYEVENTF_KEYUP,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
/// A function for easily building keyboard unicode `INPUT` structs used in `SendInput()`.
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
build_input(key_press, character, IS_REAL_KEY)
|
||||
}
|
||||
|
||||
fn send_input<I, E>(inputs: Vec<INPUT>) -> Result<()>
|
||||
/// A function for easily building keyboard virtual-key `INPUT` structs used in `SendInput()`.
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
build_input(key_press, character, IS_VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
fn send_input<I, E>(inputs: &[INPUT]) -> Result<()>
|
||||
where
|
||||
I: InputOperations,
|
||||
E: ErrorOperations,
|
||||
{
|
||||
let insert_count = I::send_input(&inputs);
|
||||
let insert_count = I::send_input(inputs);
|
||||
|
||||
if insert_count == 0 {
|
||||
let last_err = E::get_last_error().to_hresult().message();
|
||||
error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread.");
|
||||
|
||||
return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}"));
|
||||
} else if insert_count != inputs.len() as u32 {
|
||||
} else if insert_count != u32::try_from(inputs.len()).expect("to convert inputs len to u32") {
|
||||
let last_err = E::get_last_error().to_hresult().message();
|
||||
error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err,
|
||||
"SendInput sent does not match expected."
|
||||
@@ -237,8 +227,9 @@ where
|
||||
mod tests {
|
||||
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
|
||||
//! crate in order to mock those, since the mock expectations set have to be global in
|
||||
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
|
||||
//! absence of a `self`. More info: <https://docs.rs/mockall/latest/mockall/#static-methods>
|
||||
|
||||
use itertools::Itertools;
|
||||
use serial_test::serial;
|
||||
use windows::Win32::Foundation::WIN32_ERROR;
|
||||
|
||||
@@ -249,7 +240,7 @@ mod tests {
|
||||
fn get_alphabetic_hot_key_succeeds() {
|
||||
for c in ('a'..='z').chain('A'..='Z') {
|
||||
let letter = c.to_string();
|
||||
let converted = get_alphabetic_hotkey(letter).unwrap();
|
||||
let converted = get_alphabetic_hotkey(&letter).unwrap();
|
||||
assert_eq!(converted, c as u16);
|
||||
}
|
||||
}
|
||||
@@ -258,14 +249,14 @@ mod tests {
|
||||
#[should_panic = "Final keyboard shortcut key should be a single character: foo"]
|
||||
fn get_alphabetic_hot_key_fail_not_single_char() {
|
||||
let letter = String::from("foo");
|
||||
get_alphabetic_hotkey(letter).unwrap();
|
||||
get_alphabetic_hotkey(&letter).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '}'"]
|
||||
fn get_alphabetic_hot_key_fail_not_alphabetic() {
|
||||
let letter = String::from("}");
|
||||
get_alphabetic_hotkey(letter).unwrap();
|
||||
get_alphabetic_hotkey(&letter).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +266,7 @@ mod tests {
|
||||
ctxi.checkpoint();
|
||||
ctxi.expect().returning(|_| 1);
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
@@ -284,6 +275,29 @@ mod tests {
|
||||
drop(ctxi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn keyboard_shortcut_conversion_succeeds() {
|
||||
let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "B"];
|
||||
let _: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(*s))
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '1'"]
|
||||
fn keyboard_shortcut_conversion_fails_invalid_key() {
|
||||
let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "1"];
|
||||
let _: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(*s))
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic(
|
||||
@@ -298,7 +312,7 @@ mod tests {
|
||||
ctxge.checkpoint();
|
||||
ctxge.expect().returning(|| WIN32_ERROR(1));
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
@@ -320,7 +334,7 @@ mod tests {
|
||||
ctxge.checkpoint();
|
||||
ctxge.expect().returning(|| WIN32_ERROR(1));
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
|
||||
@@ -11,10 +11,10 @@ use super::{ErrorOperations, Win32ErrorOperations, WIN32_SUCCESS};
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
trait WindowHandleOperations {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw>
|
||||
fn get_window_text_length_w(&self) -> Result<i32>;
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw>
|
||||
fn get_window_text_w(&self, buffer: &mut Vec<u16>) -> Result<i32>;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ pub(super) fn get_foreground_window_title() -> Result<String> {
|
||||
|
||||
/// Retrieves the foreground window handle and validates it.
|
||||
fn get_foreground_window_handle() -> Result<WindowHandle> {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow>
|
||||
let handle = unsafe { GetForegroundWindow() };
|
||||
|
||||
debug!("GetForegroundWindow() called.");
|
||||
@@ -87,7 +87,7 @@ fn get_foreground_window_handle() -> Result<WindowHandle> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - If the length zero and GetLastError() != 0, return the GetLastError() message.
|
||||
/// - If the length zero and `GetLastError()` != 0, return the `GetLastError()` message.
|
||||
fn get_window_title_length<H, E>(window_handle: &H) -> Result<usize>
|
||||
where
|
||||
H: WindowHandleOperations,
|
||||
@@ -128,7 +128,7 @@ where
|
||||
/// # Errors
|
||||
///
|
||||
/// - If the actual window title length (what the win32 API declares was written into the buffer),
|
||||
/// is length zero and GetLastError() != 0 , return the GetLastError() message.
|
||||
/// is length zero and `GetLastError()` != 0 , return the `GetLastError()` message.
|
||||
fn get_window_title<H, E>(window_handle: &H, expected_title_length: usize) -> Result<String>
|
||||
where
|
||||
H: WindowHandleOperations,
|
||||
@@ -140,7 +140,7 @@ where
|
||||
// The upstream will make a contains comparison on what we return, so an empty string
|
||||
// will not result on a match.
|
||||
warn!("Window title length is zero.");
|
||||
return Ok(String::from(""));
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut buffer: Vec<u16> = vec![0; expected_title_length + 1]; // add extra space for the null character
|
||||
@@ -171,7 +171,7 @@ where
|
||||
mod tests {
|
||||
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
|
||||
//! crate in order to mock those, since the mock expectations set have to be global in
|
||||
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
|
||||
//! absence of a `self`. More info: <https://docs.rs/mockall/latest/mockall/#static-methods>
|
||||
|
||||
use mockall::predicate;
|
||||
use serial_test::serial;
|
||||
|
||||
@@ -1241,8 +1241,7 @@ pub mod autotype {
|
||||
input: Vec<u16>,
|
||||
keyboard_shortcut: Vec<String>,
|
||||
) -> napi::Result<(), napi::Status> {
|
||||
autotype::type_input(input, keyboard_shortcut).map_err(|_| {
|
||||
napi::Error::from_reason("Autotype Error: failed to type input".to_string())
|
||||
})
|
||||
autotype::type_input(&input, &keyboard_shortcut)
|
||||
.map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"productName": "Bitwarden Beta",
|
||||
"appId": "com.bitwarden.desktop.beta",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"productName": "Bitwarden",
|
||||
"appId": "com.bitwarden.desktop",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
@@ -70,6 +70,7 @@ import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
@@ -198,6 +199,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private readonly tokenService: TokenService,
|
||||
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
|
||||
private readonly lockService: LockService,
|
||||
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
|
||||
@@ -305,7 +307,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
|
||||
break;
|
||||
case "openPremium":
|
||||
this.dialogService.open(PremiumComponent);
|
||||
await this.premiumUpgradePromptService.promptForPremium();
|
||||
break;
|
||||
case "showFingerprintPhrase": {
|
||||
const activeUserId = await firstValueFrom(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { AssignCollectionsComponent } from "@bitwarden/vault";
|
||||
|
||||
@@ -15,6 +16,7 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { DesktopPremiumUpgradePromptService } from "../services/desktop-premium-upgrade-prompt.service";
|
||||
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
|
||||
@@ -51,7 +53,13 @@ import { SharedModule } from "./shared/shared.module";
|
||||
PremiumComponent,
|
||||
SearchComponent,
|
||||
],
|
||||
providers: [SshAgentService],
|
||||
providers: [
|
||||
SshAgentService,
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -4,7 +4,9 @@ import { RouterModule } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
@@ -36,6 +38,8 @@ describe("DesktopLayoutComponent", () => {
|
||||
let component: DesktopLayoutComponent;
|
||||
let fixture: ComponentFixture<DesktopLayoutComponent>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopLayoutComponent, RouterModule.forRoot([]), NavigationModule],
|
||||
@@ -44,6 +48,10 @@ describe("DesktopLayoutComponent", () => {
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
|
||||
@@ -24,6 +26,8 @@ describe("DesktopSideNavComponent", () => {
|
||||
let component: DesktopSideNavComponent;
|
||||
let fixture: ComponentFixture<DesktopSideNavComponent>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopSideNavComponent, NavigationModule],
|
||||
@@ -32,6 +36,10 @@ describe("DesktopSideNavComponent", () => {
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { RouterTestingHarness } from "@angular/router/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { SendFiltersNavComponent } from "./send-filters-nav.component";
|
||||
|
||||
@@ -35,6 +37,8 @@ describe("SendFiltersNavComponent", () => {
|
||||
let filterFormValueSubject: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let mockSendListFiltersService: Partial<SendListFiltersService>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
filterFormValueSubject = new BehaviorSubject<{ sendType: SendType | null }>({
|
||||
sendType: null,
|
||||
@@ -72,6 +76,10 @@ describe("SendFiltersNavComponent", () => {
|
||||
t: jest.fn((key) => key),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -4388,6 +4388,9 @@
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
"message": "添加附件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "项目已传输"
|
||||
"message": "项目已转移"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "修复加密"
|
||||
@@ -4454,7 +4454,7 @@
|
||||
"message": "我该如何管理我的密码库?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "传输项目到 $ORGANIZATION$",
|
||||
"message": "转移项目到 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4463,7 +4463,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4472,7 +4472,7 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "接受传输"
|
||||
"message": "接受转移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "拒绝并退出"
|
||||
|
||||
@@ -4,26 +4,24 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service";
|
||||
|
||||
describe("DesktopPremiumUpgradePromptService", () => {
|
||||
let service: DesktopPremiumUpgradePromptService;
|
||||
let messager: MockProxy<MessagingService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
messager = mock<MessagingService>();
|
||||
configService = mock<ConfigService>();
|
||||
dialogService = mock<DialogService>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DesktopPremiumUpgradePromptService,
|
||||
{ provide: MessagingService, useValue: messager },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
],
|
||||
@@ -52,10 +50,10 @@ describe("DesktopPremiumUpgradePromptService", () => {
|
||||
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
|
||||
);
|
||||
expect(openSpy).toHaveBeenCalledWith(dialogService);
|
||||
expect(messager.send).not.toHaveBeenCalled();
|
||||
expect(dialogService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends openPremium message when feature flag is disabled", async () => {
|
||||
it("opens the PremiumComponent when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
await service.promptForPremium();
|
||||
@@ -63,7 +61,7 @@ describe("DesktopPremiumUpgradePromptService", () => {
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
|
||||
);
|
||||
expect(messager.send).toHaveBeenCalledWith("openPremium");
|
||||
expect(dialogService.open).toHaveBeenCalledWith(PremiumComponent);
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,15 @@ import { inject } from "@angular/core";
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
|
||||
/**
|
||||
* This class handles the premium upgrade process for the desktop.
|
||||
*/
|
||||
export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService {
|
||||
private messagingService = inject(MessagingService);
|
||||
private configService = inject(ConfigService);
|
||||
private dialogService = inject(DialogService);
|
||||
|
||||
@@ -23,7 +23,7 @@ export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptS
|
||||
if (showNewDialog) {
|
||||
PremiumUpgradeDialogComponent.open(this.dialogService);
|
||||
} else {
|
||||
this.messagingService.send("openPremium");
|
||||
this.dialogService.open(PremiumComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +565,7 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
}
|
||||
}
|
||||
|
||||
if (!cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) {
|
||||
if (userCanArchive && !cipher.isDeleted && !cipher.isArchived) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("archiveVerb"),
|
||||
click: async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<authors>Bitwarden Inc.</authors>
|
||||
<projectUrl>https://bitwarden.com/</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png</iconUrl>
|
||||
<copyright>Copyright © 2015-2025 Bitwarden Inc.</copyright>
|
||||
<copyright>Copyright © 2015-2026 Bitwarden Inc.</copyright>
|
||||
<projectSourceUrl>https://github.com/bitwarden/clients/</projectSourceUrl>
|
||||
<docsUrl>https://bitwarden.com/help/</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/bitwarden/clients/issues</bugTrackerUrl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.12.1",
|
||||
"version": "2025.12.2",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -587,6 +587,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -16,27 +16,26 @@
|
||||
<img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
|
||||
<ul class="bwi-ul">
|
||||
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
|
||||
<i class="bwi bwi-li bwi-key"></i>
|
||||
<span *ngIf="!k.configured || !k.name" bitTypography="body1" class="tw-font-medium">
|
||||
{{ "webAuthnkeyX" | i18n: (i + 1).toString() }}
|
||||
</span>
|
||||
<span *ngIf="k.configured && k.name" bitTypography="body1" class="tw-font-medium">
|
||||
{{ k.name }}
|
||||
</span>
|
||||
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
|
||||
<ng-container *ngIf="k.migrated">
|
||||
<span>{{ "webAuthnMigrated" | i18n }}</span>
|
||||
<ng-container *ngIf="k.configured">
|
||||
<i class="bwi bwi-li bwi-key"></i>
|
||||
<span *ngIf="k.configured" bitTypography="body1" class="tw-font-medium">
|
||||
{{ k.name || ("unnamedKey" | i18n) }}
|
||||
</span>
|
||||
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
|
||||
<ng-container *ngIf="k.migrated">
|
||||
<span>{{ "webAuthnMigrated" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i
|
||||
class="bwi bwi-spin bwi-spinner tw-text-muted bwi-fw"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="$any(removeKeyBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
-
|
||||
<a bitLink href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i
|
||||
class="bwi bwi-spin bwi-spinner tw-text-muted bwi-fw"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="$any(removeKeyBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
-
|
||||
<a bitLink href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -60,7 +59,9 @@
|
||||
type="button"
|
||||
[bitAction]="readKey"
|
||||
buttonType="secondary"
|
||||
[disabled]="$any(readKeyBtn).loading() || webAuthnListening || !keyIdAvailable"
|
||||
[disabled]="
|
||||
$any(readKeyBtn).loading() || webAuthnListening || !keyIdAvailable || formGroup.invalid
|
||||
"
|
||||
class="tw-mr-2"
|
||||
#readKeyBtn
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, NgZone } from "@angular/core";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -99,7 +99,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
toastService,
|
||||
);
|
||||
this.formGroup = new FormGroup({
|
||||
name: new FormControl({ value: "", disabled: false }),
|
||||
name: new FormControl({ value: "", disabled: false }, Validators.required),
|
||||
});
|
||||
this.auth(data);
|
||||
}
|
||||
@@ -213,7 +213,22 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
this.webAuthnListening = listening;
|
||||
}
|
||||
|
||||
private findNextAvailableKeyId(existingIds: Set<number>): number {
|
||||
// Search for first gap, bounded by current key count + 1
|
||||
for (let i = 1; i <= existingIds.size + 1; i++) {
|
||||
if (!existingIds.has(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// This should never be reached due to loop bounds, but TypeScript requires a return
|
||||
throw new Error("Unable to find next available key ID");
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorWebAuthnResponse) {
|
||||
if (!response.keys || response.keys.length === 0) {
|
||||
response.keys = [];
|
||||
}
|
||||
this.resetWebAuthn();
|
||||
this.keys = [];
|
||||
this.keyIdAvailable = null;
|
||||
@@ -223,26 +238,37 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
nameControl.setValue("");
|
||||
}
|
||||
this.keysConfiguredCount = 0;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (response.keys != null) {
|
||||
const key = response.keys.filter((k) => k.id === i);
|
||||
if (key.length > 0) {
|
||||
this.keysConfiguredCount++;
|
||||
this.keys.push({
|
||||
id: i,
|
||||
name: key[0].name,
|
||||
configured: true,
|
||||
migrated: key[0].migrated,
|
||||
removePromise: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.keys.push({ id: i, name: "", configured: false, removePromise: null });
|
||||
if (this.keyIdAvailable == null) {
|
||||
this.keyIdAvailable = i;
|
||||
}
|
||||
|
||||
// Build configured keys
|
||||
for (const key of response.keys) {
|
||||
this.keysConfiguredCount++;
|
||||
this.keys.push({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
configured: true,
|
||||
migrated: key.migrated,
|
||||
removePromise: null,
|
||||
});
|
||||
}
|
||||
|
||||
// [PM-20109]: To accommodate the existing form logic with minimal changes,
|
||||
// we need to have at least one unconfigured key slot available to the collection.
|
||||
// Prior to PM-20109, both client and server had hard checks for IDs <= 5.
|
||||
// While we don't have any technical constraints _at this time_, we should avoid
|
||||
// unbounded growth of key IDs over time as users add/remove keys;
|
||||
// this strategy gap-fills key IDs.
|
||||
const existingIds = new Set(response.keys.map((k) => k.id));
|
||||
const nextId = this.findNextAvailableKeyId(existingIds);
|
||||
|
||||
// Add unconfigured slot, which can be used to add a new key
|
||||
this.keys.push({
|
||||
id: nextId,
|
||||
name: "",
|
||||
configured: false,
|
||||
removePromise: null,
|
||||
});
|
||||
this.keyIdAvailable = nextId;
|
||||
|
||||
this.enabled = response.enabled;
|
||||
this.onUpdated.emit(this.enabled);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<app-display-billing-address
|
||||
[subscriber]="view.organization"
|
||||
[billingAddress]="view.billingAddress"
|
||||
[taxIdWarning]="enableTaxIdWarning ? view.taxIdWarning : null"
|
||||
[taxIdWarning]="view.taxIdWarning"
|
||||
(updated)="setBillingAddress($event)"
|
||||
></app-display-billing-address>
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
|
||||
@@ -118,12 +116,9 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected enableTaxIdWarning!: boolean;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
private messageListener: MessageListener,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -140,36 +135,30 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
await this.changePaymentMethod();
|
||||
}
|
||||
|
||||
this.enableTaxIdWarning = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM22415_TaxIDWarnings,
|
||||
);
|
||||
|
||||
if (this.enableTaxIdWarning) {
|
||||
this.organizationWarningsService.taxIdWarningRefreshed$
|
||||
.pipe(
|
||||
switchMap((warning) =>
|
||||
combineLatest([
|
||||
of(warning),
|
||||
this.organization$.pipe(take(1)).pipe(
|
||||
mapOrganizationToSubscriber,
|
||||
switchMap((organization) =>
|
||||
this.subscriberBillingClient.getBillingAddress(organization),
|
||||
),
|
||||
this.organizationWarningsService.taxIdWarningRefreshed$
|
||||
.pipe(
|
||||
switchMap((warning) =>
|
||||
combineLatest([
|
||||
of(warning),
|
||||
this.organization$.pipe(take(1)).pipe(
|
||||
mapOrganizationToSubscriber,
|
||||
switchMap((organization) =>
|
||||
this.subscriberBillingClient.getBillingAddress(organization),
|
||||
),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([taxIdWarning, billingAddress]) => {
|
||||
if (this.viewState$.value) {
|
||||
this.viewState$.next({
|
||||
...this.viewState$.value,
|
||||
taxIdWarning,
|
||||
billingAddress,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([taxIdWarning, billingAddress]) => {
|
||||
if (this.viewState$.value) {
|
||||
this.viewState$.next({
|
||||
...this.viewState$.value,
|
||||
taxIdWarning,
|
||||
billingAddress,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.messageListener
|
||||
.messages$(BANK_ACCOUNT_VERIFIED_COMMAND)
|
||||
@@ -216,10 +205,7 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
|
||||
setBillingAddress = (billingAddress: BillingAddress) => {
|
||||
if (this.viewState$.value) {
|
||||
if (
|
||||
this.enableTaxIdWarning &&
|
||||
this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId
|
||||
) {
|
||||
if (this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId) {
|
||||
this.organizationWarningsService.refreshTaxIdWarning();
|
||||
}
|
||||
this.viewState$.next({
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
|
||||
import { Account, 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { BannerModule, DialogService } from "@bitwarden/components";
|
||||
import { BILLING_DISK, StateProvider, UserKeyDefinition } from "@bitwarden/state";
|
||||
@@ -88,23 +86,21 @@ type GetWarning$ = () => Observable<TaxIdWarningType | null>;
|
||||
@Component({
|
||||
selector: "app-tax-id-warning",
|
||||
template: `
|
||||
@if (enableTaxIdWarning$ | async) {
|
||||
@let view = view$ | async;
|
||||
@let view = view$ | async;
|
||||
|
||||
@if (view) {
|
||||
<bit-banner id="tax-id-warning-banner" bannerType="warning" (onClose)="trackDismissal()">
|
||||
{{ view.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="editBillingAddress()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ view.callToAction }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
}
|
||||
@if (view) {
|
||||
<bit-banner id="tax-id-warning-banner" bannerType="warning" (onClose)="trackDismissal()">
|
||||
{{ view.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="editBillingAddress()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ view.callToAction }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
}
|
||||
`,
|
||||
imports: [BannerModule, SharedModule],
|
||||
@@ -120,10 +116,6 @@ export class TaxIdWarningComponent implements OnInit {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() billingAddressUpdated = new EventEmitter<void>();
|
||||
|
||||
protected enableTaxIdWarning$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM22415_TaxIDWarnings,
|
||||
);
|
||||
|
||||
protected userId$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => account !== null),
|
||||
getUserId,
|
||||
@@ -209,7 +201,6 @@ export class TaxIdWarningComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</bit-toggle>
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
|
||||
<ng-container header>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</bit-toggle>
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
|
||||
<ng-container header>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
|
||||
@@ -132,7 +132,10 @@ describe("CipherStep", () => {
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
ciphers: [
|
||||
{ id: "cipher-1", organizationId: null } as Cipher,
|
||||
{ id: "cipher-2", organizationId: null } as Cipher,
|
||||
],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
@@ -144,14 +147,39 @@ describe("CipherStep", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when there are undecryptable ciphers", async () => {
|
||||
it("returns true when there are undecryptable ciphers but at least one decryptable cipher", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
ciphers: [
|
||||
{ id: "cipher-1", organizationId: null } as Cipher,
|
||||
{ id: "cipher-2", organizationId: null } as Cipher,
|
||||
],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockRejectedValueOnce(new Error("Decryption failed"));
|
||||
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
const result = cipherStep.canRecover(workingData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when all ciphers are undecryptable", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [
|
||||
{ id: "cipher-1", organizationId: null } as Cipher,
|
||||
{ id: "cipher-2", organizationId: null } as Cipher,
|
||||
],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
@@ -160,7 +188,7 @@ describe("CipherStep", () => {
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
const result = cipherStep.canRecover(workingData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export class CipherStep implements RecoveryStep {
|
||||
title = "recoveryStepCipherTitle";
|
||||
|
||||
private undecryptableCipherIds: string[] = [];
|
||||
private decryptableCipherIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -31,18 +32,21 @@ export class CipherStep implements RecoveryStep {
|
||||
for (const cipher of userCiphers) {
|
||||
try {
|
||||
await this.cipherService.decrypt(cipher, workingData.userId);
|
||||
this.decryptableCipherIds.push(cipher.id);
|
||||
} catch {
|
||||
logger.record(`Cipher ID ${cipher.id} was undecryptable`);
|
||||
this.undecryptableCipherIds.push(cipher.id);
|
||||
}
|
||||
}
|
||||
logger.record(`Found ${this.undecryptableCipherIds.length} undecryptable ciphers`);
|
||||
logger.record(`Found ${this.decryptableCipherIds.length} decryptable ciphers`);
|
||||
|
||||
return this.undecryptableCipherIds.length == 0;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return this.undecryptableCipherIds.length > 0;
|
||||
// If everything fails to decrypt, it's a deeper issue and we shouldn't offer recovery here.
|
||||
return this.undecryptableCipherIds.length > 0 && this.decryptableCipherIds.length > 0;
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
|
||||
@@ -11,6 +11,7 @@ export class FolderStep implements RecoveryStep {
|
||||
title = "recoveryStepFoldersTitle";
|
||||
|
||||
private undecryptableFolderIds: string[] = [];
|
||||
private decryptableFolderIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private folderService: FolderApiServiceAbstraction,
|
||||
@@ -36,18 +37,21 @@ export class FolderStep implements RecoveryStep {
|
||||
folder.name.encryptedString,
|
||||
workingData.userKey.toEncoded(),
|
||||
);
|
||||
this.decryptableFolderIds.push(folder.id);
|
||||
} catch {
|
||||
logger.record(`Folder name for folder ID ${folder.id} was undecryptable`);
|
||||
this.undecryptableFolderIds.push(folder.id);
|
||||
}
|
||||
}
|
||||
logger.record(`Found ${this.undecryptableFolderIds.length} undecryptable folders`);
|
||||
logger.record(`Found ${this.decryptableFolderIds.length} decryptable folders`);
|
||||
|
||||
return this.undecryptableFolderIds.length == 0;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return this.undecryptableFolderIds.length > 0;
|
||||
// If everything fails to decrypt, it's a deeper issue and we shouldn't offer recovery here.
|
||||
return this.undecryptableFolderIds.length > 0 && this.decryptableFolderIds.length > 0;
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
|
||||
@@ -7,10 +7,12 @@ import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { IconButtonModule, NavigationModule } from "@bitwarden/components";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service";
|
||||
|
||||
@@ -59,6 +61,8 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
productSwitcherService.shouldShowPremiumUpgradeButton$ = mockShouldShowPremiumUpgradeButton$;
|
||||
mockProducts$.next({ bento: [], other: [] });
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RouterModule, NavigationModule, IconButtonModule, MockUpgradeNavButtonComponent],
|
||||
declarations: [NavigationProductSwitcherComponent, I18nPipe],
|
||||
@@ -72,6 +76,10 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
provide: ActivatedRoute,
|
||||
useValue: mock<ActivatedRoute>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -16,10 +16,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import {
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
StorybookGlobalStateProvider,
|
||||
} from "@bitwarden/components";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { ProductSwitcherService } from "../shared/product-switcher.service";
|
||||
@@ -82,7 +87,7 @@ class MockAccountService implements Partial<AccountService> {
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
creationDate: new Date("2024-01-01T00:00:00.000Z"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,6 +188,10 @@ export default {
|
||||
},
|
||||
]),
|
||||
),
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useClass: StorybookGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -82,7 +82,7 @@ class MockAccountService implements Partial<AccountService> {
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
creationDate: new Date("2024-01-01T00:00:00.000Z"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { LayoutComponent } from "@bitwarden/components";
|
||||
import { LayoutComponent, StorybookGlobalStateProvider } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
@@ -168,6 +169,10 @@ export default {
|
||||
providers: [
|
||||
importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useClass: StorybookGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -74,6 +74,9 @@ export class RoutedVaultFilterService implements OnDestroy {
|
||||
type: filter.type ?? null,
|
||||
},
|
||||
queryParamsHandling: "merge",
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
};
|
||||
return [commands, extras];
|
||||
}
|
||||
|
||||
@@ -424,6 +424,9 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user