mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge branch 'main' into km/pm-26057-enforce-session-timeout-policy
This commit is contained in:
commit
4c613a2b6f
@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
27
.github/workflows/build-desktop.yml
vendored
27
.github/workflows/build-desktop.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: true
|
||||
@ -173,7 +173,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -256,6 +256,13 @@ jobs:
|
||||
- name: Build application
|
||||
run: npm run dist:lin
|
||||
|
||||
- name: Upload tar.gz artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: bitwarden_${{ env._PACKAGE_VERSION }}_x64.tar.gz
|
||||
path: apps/desktop/dist/bitwarden_desktop_x64.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .deb artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
@ -322,7 +329,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -470,7 +477,7 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -736,7 +743,7 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -979,7 +986,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -1216,7 +1223,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -1488,7 +1495,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@ -1826,7 +1833,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Checkout base branch repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
echo "Github Release Option: $_RELEASE_OPTION"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
3
.github/workflows/release-desktop.yml
vendored
3
.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -110,6 +110,7 @@ jobs:
|
||||
apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_amd64.snap,
|
||||
apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_arm64.snap,
|
||||
apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_arm64.tar.gz,
|
||||
apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_x64.tar.gz,
|
||||
apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x86_64.AppImage,
|
||||
apps/desktop/artifacts/Bitwarden-Portable-${{ env.PKG_VERSION }}.exe,
|
||||
apps/desktop/artifacts/Bitwarden-Installer-${{ env.PKG_VERSION }}.exe,
|
||||
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/repository-management.yml
vendored
4
.github/workflows/repository-management.yml
vendored
@ -104,7 +104,7 @@ jobs:
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
@ -469,7 +469,7 @@ jobs:
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@ -64,7 +64,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Check out clients repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -137,7 +137,7 @@ jobs:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -173,7 +173,7 @@ jobs:
|
||||
- rust-coverage
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@ -585,6 +585,9 @@
|
||||
"archiveItemConfirmDesc": {
|
||||
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
||||
},
|
||||
"upgradeToUseArchive": {
|
||||
"message": "A premium membership is required to use Archive."
|
||||
},
|
||||
"edit": {
|
||||
"message": "Edit"
|
||||
},
|
||||
@ -594,6 +597,9 @@
|
||||
"viewAll": {
|
||||
"message": "View all"
|
||||
},
|
||||
"showAll": {
|
||||
"message": "Show all"
|
||||
},
|
||||
"viewLess": {
|
||||
"message": "View less"
|
||||
},
|
||||
@ -4902,6 +4908,9 @@
|
||||
"premium": {
|
||||
"message": "Premium"
|
||||
},
|
||||
"unlockFeaturesWithPremium": {
|
||||
"message": "Unlock reporting, emergency access, and more security features with Premium."
|
||||
},
|
||||
"freeOrgsCannotUseAttachments": {
|
||||
"message": "Free organizations cannot use attachments"
|
||||
},
|
||||
@ -5812,11 +5821,11 @@
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"upgradeCompleteSecurity": {
|
||||
"message": "Upgrade for complete security"
|
||||
"unlockAdvancedSecurity": {
|
||||
"message": "Unlock advanced security features"
|
||||
},
|
||||
"premiumGivesMoreTools": {
|
||||
"message": "Premium gives you more tools to stay secure, work efficiently, and stay in control."
|
||||
"unlockAdvancedSecurityDesc": {
|
||||
"message": "A Premium subscription gives you more tools to stay secure and in control"
|
||||
},
|
||||
"explorePremium": {
|
||||
"message": "Explore Premium"
|
||||
|
||||
@ -1344,7 +1344,7 @@ export default class NotificationBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionUrl = chrome.runtime.getURL("popup/index.html");
|
||||
const extensionUrl = BrowserApi.getRuntimeURL("popup/index.html");
|
||||
const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter(
|
||||
(tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`),
|
||||
);
|
||||
|
||||
@ -2949,13 +2949,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
(await this.checkFocusedFieldHasValue(port.sender.tab)) &&
|
||||
(await this.shouldShowSaveLoginInlineMenuList(port.sender.tab));
|
||||
|
||||
const iframeUrl = chrome.runtime.getURL(
|
||||
const iframeUrl = BrowserApi.getRuntimeURL(
|
||||
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
|
||||
);
|
||||
const styleSheetUrl = chrome.runtime.getURL(
|
||||
const styleSheetUrl = BrowserApi.getRuntimeURL(
|
||||
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
|
||||
);
|
||||
const extensionOrigin = new URL(iframeUrl).origin;
|
||||
const extensionOrigin = iframeUrl ? new URL(iframeUrl).origin : null;
|
||||
|
||||
this.postMessageToPort(port, {
|
||||
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
|
||||
|
||||
@ -56,7 +56,11 @@ describe("ContentMessageHandler", () => {
|
||||
});
|
||||
|
||||
it("sends an authResult message", () => {
|
||||
postWindowMessage({ command: "authResult", lastpass: true, code: "code", state: "state" });
|
||||
postWindowMessage(
|
||||
{ command: "authResult", lastpass: true, code: "code", state: "state" },
|
||||
"https://localhost/",
|
||||
window,
|
||||
);
|
||||
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||
command: "authResult",
|
||||
@ -68,7 +72,11 @@ describe("ContentMessageHandler", () => {
|
||||
});
|
||||
|
||||
it("sends a webAuthnResult message", () => {
|
||||
postWindowMessage({ command: "webAuthnResult", data: "data", remember: true });
|
||||
postWindowMessage(
|
||||
{ command: "webAuthnResult", data: "data", remember: true },
|
||||
"https://localhost/",
|
||||
window,
|
||||
);
|
||||
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||
command: "webAuthnResult",
|
||||
@ -82,7 +90,7 @@ describe("ContentMessageHandler", () => {
|
||||
const mockCode = "mockCode";
|
||||
const command = "duoResult";
|
||||
|
||||
postWindowMessage({ command: command, code: mockCode });
|
||||
postWindowMessage({ command: command, code: mockCode }, "https://localhost/", window);
|
||||
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||
command: command,
|
||||
|
||||
@ -51,6 +51,7 @@ type NotificationBarWindowMessage = {
|
||||
};
|
||||
error?: string;
|
||||
initData?: NotificationBarIframeInitData;
|
||||
parentOrigin?: string;
|
||||
};
|
||||
|
||||
type NotificationBarWindowMessageHandlers = {
|
||||
|
||||
121
apps/browser/src/autofill/notification/bar.spec.ts
Normal file
121
apps/browser/src/autofill/notification/bar.spec.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { postWindowMessage } from "../spec/testing-utils";
|
||||
|
||||
import { NotificationBarWindowMessage } from "./abstractions/notification-bar";
|
||||
import "./bar";
|
||||
|
||||
jest.mock("lit", () => ({ render: jest.fn() }));
|
||||
jest.mock("@lit-labs/signals", () => ({
|
||||
signal: jest.fn((testValue) => ({ get: (): typeof testValue => testValue })),
|
||||
}));
|
||||
jest.mock("../content/components/notification/container", () => ({
|
||||
NotificationContainer: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("NotificationBar iframe handleWindowMessage security", () => {
|
||||
const trustedOrigin = "http://localhost";
|
||||
const maliciousOrigin = "https://malicious.com";
|
||||
|
||||
const createMessage = (
|
||||
overrides: Partial<NotificationBarWindowMessage> = {},
|
||||
): NotificationBarWindowMessage => ({
|
||||
command: "initNotificationBar",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(globalThis, "location", {
|
||||
value: { search: `?parentOrigin=${encodeURIComponent(trustedOrigin)}` },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(globalThis, "parent", {
|
||||
value: mock<Window>(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
globalThis.dispatchEvent(new Event("load"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: "not from parent window",
|
||||
message: () => createMessage(),
|
||||
origin: trustedOrigin,
|
||||
source: () => mock<Window>(),
|
||||
},
|
||||
{
|
||||
description: "with mismatched origin",
|
||||
message: () => createMessage(),
|
||||
origin: maliciousOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
{
|
||||
description: "without command field",
|
||||
message: () => ({}),
|
||||
origin: trustedOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
{
|
||||
description: "initNotificationBar with mismatched parentOrigin",
|
||||
message: () => createMessage({ parentOrigin: maliciousOrigin }),
|
||||
origin: trustedOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
{
|
||||
description: "when windowMessageOrigin is not set",
|
||||
message: () => createMessage(),
|
||||
origin: "different-origin",
|
||||
source: () => globalThis.parent,
|
||||
resetOrigin: true,
|
||||
},
|
||||
{
|
||||
description: "with null source",
|
||||
message: () => createMessage(),
|
||||
origin: trustedOrigin,
|
||||
source: (): null => null,
|
||||
},
|
||||
{
|
||||
description: "with unknown command",
|
||||
message: () => createMessage({ command: "unknownCommand" }),
|
||||
origin: trustedOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
])("should reject messages $description", ({ message, origin, source, resetOrigin }) => {
|
||||
if (resetOrigin) {
|
||||
Object.defineProperty(globalThis, "location", {
|
||||
value: { search: "" },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||
postWindowMessage(message(), origin, source());
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should accept and handle valid trusted messages", () => {
|
||||
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||
spy.mockClear();
|
||||
|
||||
const validMessage = createMessage({
|
||||
parentOrigin: trustedOrigin,
|
||||
initData: {
|
||||
type: "change",
|
||||
isVaultLocked: false,
|
||||
removeIndividualVault: false,
|
||||
importType: null,
|
||||
launchTimestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
postWindowMessage(validMessage, trustedOrigin, globalThis.parent);
|
||||
expect(validMessage.command).toBe("initNotificationBar");
|
||||
expect(validMessage.parentOrigin).toBe(trustedOrigin);
|
||||
expect(validMessage.initData).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -24,6 +24,13 @@ import {
|
||||
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
|
||||
let windowMessageOrigin: string;
|
||||
|
||||
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||
const trustedParentOrigin = urlParams.get("parentOrigin");
|
||||
|
||||
if (trustedParentOrigin) {
|
||||
windowMessageOrigin = trustedParentOrigin;
|
||||
}
|
||||
|
||||
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
||||
initNotificationBar: ({ message }) => initNotificationBar(message),
|
||||
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message),
|
||||
@ -395,15 +402,27 @@ function setupWindowMessageListener() {
|
||||
}
|
||||
|
||||
function handleWindowMessage(event: MessageEvent) {
|
||||
if (!windowMessageOrigin) {
|
||||
windowMessageOrigin = event.origin;
|
||||
}
|
||||
|
||||
if (event.origin !== windowMessageOrigin) {
|
||||
if (event?.source !== globalThis.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data as NotificationBarWindowMessage;
|
||||
if (!message?.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!windowMessageOrigin || event.origin !== windowMessageOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
message.command === "initNotificationBar" &&
|
||||
message.parentOrigin &&
|
||||
message.parentOrigin !== event.origin
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = notificationBarWindowMessageHandlers[message.command];
|
||||
if (!handler) {
|
||||
return;
|
||||
@ -431,5 +450,8 @@ function getResolvedTheme(theme: Theme) {
|
||||
}
|
||||
|
||||
function postMessageToParent(message: NotificationBarWindowMessage) {
|
||||
globalThis.parent.postMessage(message, windowMessageOrigin || "*");
|
||||
if (!windowMessageOrigin) {
|
||||
return;
|
||||
}
|
||||
globalThis.parent.postMessage(message, windowMessageOrigin);
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ export type InitAutofillInlineMenuButtonMessage = UpdateAuthStatusMessage & {
|
||||
styleSheetUrl: string;
|
||||
translations: Record<string, string>;
|
||||
portKey: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type AutofillInlineMenuButtonWindowMessageHandlers = {
|
||||
|
||||
@ -5,7 +5,7 @@ import { InlineMenuCipherData } from "../../../background/abstractions/overlay.b
|
||||
export type AutofillInlineMenuContainerMessage = {
|
||||
command: string;
|
||||
portKey: string;
|
||||
token?: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & {
|
||||
|
||||
@ -27,6 +27,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
|
||||
showInlineMenuAccountCreation?: boolean;
|
||||
showPasskeysLabels?: boolean;
|
||||
portKey: string;
|
||||
token: string;
|
||||
generatedPassword?: string;
|
||||
showSaveLoginMenu?: boolean;
|
||||
};
|
||||
|
||||
@ -191,7 +191,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
|
||||
expect(
|
||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||
).toHaveBeenCalledWith(message, "*");
|
||||
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||
});
|
||||
|
||||
it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
|
||||
@ -217,7 +217,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
||||
expect(
|
||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||
).toHaveBeenCalledWith(message, "*");
|
||||
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -242,7 +242,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
||||
expect(
|
||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||
).toHaveBeenCalledWith(message, "*");
|
||||
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||
});
|
||||
|
||||
it("sets a light theme based on the user's system preferences", () => {
|
||||
@ -262,7 +262,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
command: "initAutofillInlineMenuList",
|
||||
theme: ThemeType.Light,
|
||||
},
|
||||
"*",
|
||||
autofillInlineMenuIframeService["extensionOrigin"],
|
||||
);
|
||||
});
|
||||
|
||||
@ -283,7 +283,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
command: "initAutofillInlineMenuList",
|
||||
theme: ThemeType.Dark,
|
||||
},
|
||||
"*",
|
||||
autofillInlineMenuIframeService["extensionOrigin"],
|
||||
);
|
||||
});
|
||||
|
||||
@ -387,7 +387,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
command: "updateAutofillInlineMenuColorScheme",
|
||||
colorScheme: "normal",
|
||||
},
|
||||
"*",
|
||||
autofillInlineMenuIframeService["extensionOrigin"],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
||||
import {
|
||||
BackgroundPortMessageHandlers,
|
||||
@ -15,6 +16,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||
private port: chrome.runtime.Port | null = null;
|
||||
private portKey: string;
|
||||
private readonly extensionOrigin: string;
|
||||
private iframeMutationObserver: MutationObserver;
|
||||
private iframe: HTMLIFrameElement;
|
||||
private ariaAlertElement: HTMLDivElement;
|
||||
@ -69,6 +71,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
private iframeTitle: string,
|
||||
private ariaAlert?: string,
|
||||
) {
|
||||
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||
this.iframeMutationObserver = new MutationObserver(this.handleMutations);
|
||||
}
|
||||
|
||||
@ -81,7 +84,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
* that is declared.
|
||||
*/
|
||||
initMenuIframe() {
|
||||
this.defaultIframeAttributes.src = chrome.runtime.getURL("overlay/menu.html");
|
||||
this.defaultIframeAttributes.src = BrowserApi.getRuntimeURL("overlay/menu.html");
|
||||
this.defaultIframeAttributes.title = this.iframeTitle;
|
||||
|
||||
this.iframe = globalThis.document.createElement("iframe");
|
||||
@ -259,7 +262,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
}
|
||||
|
||||
private postMessageToIFrame(message: any) {
|
||||
this.iframe.contentWindow?.postMessage({ portKey: this.portKey, ...message }, "*");
|
||||
this.iframe.contentWindow?.postMessage(
|
||||
{ portKey: this.portKey, ...message },
|
||||
this.extensionOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import { createInitAutofillInlineMenuButtonMessageMock } from "../../../../spec/autofill-mocks";
|
||||
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
||||
|
||||
@ -10,11 +11,11 @@ describe("AutofillInlineMenuButton", () => {
|
||||
|
||||
let autofillInlineMenuButton: AutofillInlineMenuButton;
|
||||
const portKey: string = "inlineMenuButtonPortKey";
|
||||
const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id";
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `<autofill-inline-menu-button></autofill-inline-menu-button>`;
|
||||
autofillInlineMenuButton = document.querySelector("autofill-inline-menu-button");
|
||||
autofillInlineMenuButton["messageOrigin"] = "https://localhost/";
|
||||
jest.spyOn(globalThis.document, "createElement");
|
||||
jest.spyOn(globalThis.parent, "postMessage");
|
||||
});
|
||||
@ -56,8 +57,8 @@ describe("AutofillInlineMenuButton", () => {
|
||||
autofillInlineMenuButton["buttonElement"].click();
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "autofillInlineMenuButtonClicked", portKey },
|
||||
"*",
|
||||
{ command: "autofillInlineMenuButtonClicked", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -70,7 +71,7 @@ describe("AutofillInlineMenuButton", () => {
|
||||
it("does not post a message to close the autofill inline menu if the element is focused during the focus check", async () => {
|
||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||
await flushPromises();
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
||||
@ -84,7 +85,7 @@ describe("AutofillInlineMenuButton", () => {
|
||||
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||
await flushPromises();
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
||||
@ -98,7 +99,7 @@ describe("AutofillInlineMenuButton", () => {
|
||||
jest
|
||||
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||
await flushPromises();
|
||||
|
||||
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||
@ -113,12 +114,12 @@ describe("AutofillInlineMenuButton", () => {
|
||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||
jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null);
|
||||
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||
await flushPromises();
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "triggerDelayedAutofillInlineMenuClosure", portKey },
|
||||
"*",
|
||||
{ command: "triggerDelayedAutofillInlineMenuClosure", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -128,6 +129,7 @@ describe("AutofillInlineMenuButton", () => {
|
||||
postWindowMessage({
|
||||
command: "updateAutofillInlineMenuButtonAuthStatus",
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
token: "test-token",
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
@ -143,6 +145,7 @@ describe("AutofillInlineMenuButton", () => {
|
||||
postWindowMessage({
|
||||
command: "updateAutofillInlineMenuColorScheme",
|
||||
colorScheme: "dark",
|
||||
token: "test-token",
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||
import {
|
||||
createAutofillOverlayCipherDataMock,
|
||||
@ -23,6 +24,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
|
||||
let autofillInlineMenuList: AutofillInlineMenuList | null;
|
||||
const portKey: string = "inlineMenuListPortKey";
|
||||
const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id";
|
||||
const events: { eventName: any; callback: any }[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
@ -67,8 +69,8 @@ describe("AutofillInlineMenuList", () => {
|
||||
unlockButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "unlockVault", portKey },
|
||||
"*",
|
||||
{ command: "unlockVault", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -134,8 +136,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
addVaultItemButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
||||
"*",
|
||||
{
|
||||
command: "addNewVaultItem",
|
||||
portKey,
|
||||
addNewCipherType: CipherType.Login,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -324,8 +331,9 @@ describe("AutofillInlineMenuList", () => {
|
||||
inlineMenuCipherId: "1",
|
||||
usePasskey: false,
|
||||
portKey,
|
||||
token: "test-token",
|
||||
},
|
||||
"*",
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -492,8 +500,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
viewCipherButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "viewSelectedCipher", inlineMenuCipherId: "1", portKey },
|
||||
"*",
|
||||
{
|
||||
command: "viewSelectedCipher",
|
||||
inlineMenuCipherId: "1",
|
||||
portKey,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -581,8 +594,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
newVaultItemButtonSpy.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
||||
"*",
|
||||
{
|
||||
command: "addNewVaultItem",
|
||||
portKey,
|
||||
addNewCipherType: CipherType.Login,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -826,8 +844,8 @@ describe("AutofillInlineMenuList", () => {
|
||||
fillGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "fillGeneratedPassword", portKey },
|
||||
"*",
|
||||
{ command: "fillGeneratedPassword", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -843,7 +861,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||
{ command: "fillGeneratedPassword", portKey },
|
||||
"*",
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -857,8 +875,8 @@ describe("AutofillInlineMenuList", () => {
|
||||
);
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "fillGeneratedPassword", portKey },
|
||||
"*",
|
||||
{ command: "fillGeneratedPassword", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -896,8 +914,8 @@ describe("AutofillInlineMenuList", () => {
|
||||
refreshGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "refreshGeneratedPassword", portKey },
|
||||
"*",
|
||||
{ command: "refreshGeneratedPassword", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -913,7 +931,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||
{ command: "refreshGeneratedPassword", portKey },
|
||||
"*",
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -927,8 +945,8 @@ describe("AutofillInlineMenuList", () => {
|
||||
);
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "refreshGeneratedPassword", portKey },
|
||||
"*",
|
||||
{ command: "refreshGeneratedPassword", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -972,7 +990,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => {
|
||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -983,7 +1001,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -994,7 +1012,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
jest
|
||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||
await flushPromises();
|
||||
|
||||
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||
@ -1010,11 +1028,11 @@ describe("AutofillInlineMenuList", () => {
|
||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||
.mockReturnValue(null);
|
||||
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "checkAutofillInlineMenuButtonFocused", portKey },
|
||||
"*",
|
||||
{ command: "checkAutofillInlineMenuButtonFocused", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -1022,7 +1040,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||
const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems");
|
||||
|
||||
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers" });
|
||||
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers", token: "test-token" });
|
||||
|
||||
expect(updateCiphersSpy).toHaveBeenCalled();
|
||||
});
|
||||
@ -1062,7 +1080,10 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||
await flushPromises();
|
||||
|
||||
postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword" });
|
||||
postWindowMessage({
|
||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||
token: "test-token",
|
||||
});
|
||||
|
||||
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -1074,6 +1095,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage({
|
||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||
generatedPassword,
|
||||
token: "test-token",
|
||||
});
|
||||
|
||||
expect(buildPasswordGeneratorSpy).toHaveBeenCalled();
|
||||
@ -1090,6 +1112,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage({
|
||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||
generatedPassword,
|
||||
token: "test-token",
|
||||
});
|
||||
|
||||
expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1);
|
||||
@ -1115,7 +1138,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
||||
postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" });
|
||||
|
||||
expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -1124,7 +1147,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||
await flushPromises();
|
||||
|
||||
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
||||
postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" });
|
||||
|
||||
expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled();
|
||||
});
|
||||
@ -1143,7 +1166,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
"setAttribute",
|
||||
);
|
||||
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||
|
||||
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
|
||||
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
|
||||
@ -1161,7 +1184,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
|
||||
jest.spyOn(unlockButton as HTMLElement, "focus");
|
||||
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||
|
||||
expect((unlockButton as HTMLElement).focus).toBeCalled();
|
||||
});
|
||||
@ -1173,7 +1196,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
|
||||
jest.spyOn(newItemButton as HTMLElement, "focus");
|
||||
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||
|
||||
expect((newItemButton as HTMLElement).focus).toBeCalled();
|
||||
});
|
||||
@ -1184,7 +1207,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
|
||||
jest.spyOn(firstCipherItem as HTMLElement, "focus");
|
||||
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
||||
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||
|
||||
expect((firstCipherItem as HTMLElement).focus).toBeCalled();
|
||||
});
|
||||
@ -1197,8 +1220,8 @@ describe("AutofillInlineMenuList", () => {
|
||||
globalThis.dispatchEvent(new Event("blur"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "autofillInlineMenuBlurred", portKey },
|
||||
"*",
|
||||
{ command: "autofillInlineMenuBlurred", portKey, token: "test-token" },
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1220,8 +1243,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
);
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "previous", portKey },
|
||||
"*",
|
||||
{
|
||||
command: "redirectAutofillInlineMenuFocusOut",
|
||||
direction: "previous",
|
||||
portKey,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -1229,8 +1257,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "next", portKey },
|
||||
"*",
|
||||
{
|
||||
command: "redirectAutofillInlineMenuFocusOut",
|
||||
direction: "next",
|
||||
portKey,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
|
||||
@ -1238,8 +1271,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "current", portKey },
|
||||
"*",
|
||||
{
|
||||
command: "redirectAutofillInlineMenuFocusOut",
|
||||
direction: "current",
|
||||
portKey,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1274,8 +1312,13 @@ describe("AutofillInlineMenuList", () => {
|
||||
autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "updateAutofillInlineMenuListHeight", styles: { height: "300px" }, portKey },
|
||||
"*",
|
||||
{
|
||||
command: "updateAutofillInlineMenuListHeight",
|
||||
styles: { height: "300px" },
|
||||
portKey,
|
||||
token: "test-token",
|
||||
},
|
||||
expectedOrigin,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import { generateRandomChars, setElementStyles } from "../../../../utils";
|
||||
import {
|
||||
InitAutofillInlineMenuElementMessage,
|
||||
@ -73,7 +74,7 @@ export class AutofillInlineMenuContainer {
|
||||
|
||||
constructor() {
|
||||
this.token = generateRandomChars(32);
|
||||
this.extensionOrigin = chrome.runtime.getURL("").slice(0, -1);
|
||||
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||
globalThis.addEventListener("message", this.handleWindowMessage);
|
||||
}
|
||||
|
||||
@ -203,6 +204,9 @@ export class AutofillInlineMenuContainer {
|
||||
*/
|
||||
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
||||
const message = event.data;
|
||||
if (!message?.command) {
|
||||
return;
|
||||
}
|
||||
if (this.isForeignWindowMessage(event)) {
|
||||
return;
|
||||
}
|
||||
@ -287,7 +291,10 @@ export class AutofillInlineMenuContainer {
|
||||
* every time the inline menu container is recreated.
|
||||
*
|
||||
*/
|
||||
private isValidSessionToken(message: { token?: string }): boolean {
|
||||
private isValidSessionToken(message: { token: string }): boolean {
|
||||
if (!this.token || !message?.token || !message?.token.length) {
|
||||
return false;
|
||||
}
|
||||
return message.token === this.token;
|
||||
}
|
||||
|
||||
|
||||
@ -38,12 +38,8 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||
styleSheetUrl: string,
|
||||
translations: Record<string, string>,
|
||||
portKey: string,
|
||||
token?: string,
|
||||
): Promise<HTMLLinkElement> {
|
||||
this.portKey = portKey;
|
||||
if (token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
this.translations = translations;
|
||||
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
||||
@ -63,11 +59,16 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||
* @param message - The message to post
|
||||
*/
|
||||
protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) {
|
||||
const messageWithAuth: Record<string, unknown> = { portKey: this.portKey, ...message };
|
||||
if (this.token) {
|
||||
messageWithAuth.token = this.token;
|
||||
// never send messages containing authentication tokens without a valid token and an established messageOrigin
|
||||
if (!this.token || !this.messageOrigin) {
|
||||
return;
|
||||
}
|
||||
globalThis.parent.postMessage(messageWithAuth, "*");
|
||||
const messageWithAuth: Record<string, unknown> = {
|
||||
portKey: this.portKey,
|
||||
...message,
|
||||
token: this.token,
|
||||
};
|
||||
globalThis.parent.postMessage(messageWithAuth, this.messageOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,6 +106,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.source !== globalThis.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.messageOrigin) {
|
||||
this.messageOrigin = event.origin;
|
||||
}
|
||||
@ -115,12 +120,23 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||
|
||||
const message = event?.data;
|
||||
|
||||
if (
|
||||
message?.token &&
|
||||
(message?.command === "initAutofillInlineMenuButton" ||
|
||||
message?.command === "initAutofillInlineMenuList")
|
||||
) {
|
||||
if (!message?.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isInitCommand =
|
||||
message.command === "initAutofillInlineMenuButton" ||
|
||||
message.command === "initAutofillInlineMenuList";
|
||||
|
||||
if (isInitCommand) {
|
||||
if (!message?.token) {
|
||||
return;
|
||||
}
|
||||
this.token = message.token;
|
||||
} else {
|
||||
if (!this.token || !message?.token || message.token !== this.token) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const handler = this.windowMessageHandlers[message?.command];
|
||||
|
||||
@ -7,7 +7,7 @@ exports[`OverlayNotificationsContentService opening the notification bar creates
|
||||
>
|
||||
<iframe
|
||||
id="bit-notification-bar-iframe"
|
||||
src="chrome-extension://id/notification/bar.html"
|
||||
src="chrome-extension://id/notification/bar.html?parentOrigin=http%3A%2F%2Flocalhost"
|
||||
style="width: 100% !important; height: 100% !important; border: 0px !important; display: block !important; position: relative !important; transition: transform 0.15s ease-out, opacity 0.15s ease !important; border-radius: 4px !important; color-scheme: auto !important; transform: translateX(0) !important; opacity: 0;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -155,8 +155,9 @@ describe("OverlayNotificationsContentService", () => {
|
||||
{
|
||||
command: "initNotificationBar",
|
||||
initData: expect.any(Object),
|
||||
parentOrigin: expect.any(String),
|
||||
},
|
||||
"*",
|
||||
overlayNotificationsContentService["extensionOrigin"],
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -257,7 +258,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
|
||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||
{ command: "saveCipherAttemptCompleted", error: undefined },
|
||||
"*",
|
||||
overlayNotificationsContentService["extensionOrigin"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import {
|
||||
NotificationBarIframeInitData,
|
||||
NotificationType,
|
||||
@ -22,6 +23,7 @@ export class OverlayNotificationsContentService
|
||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||
private notificationBarShadowRoot: ShadowRoot | null = null;
|
||||
private currentNotificationBarType: NotificationType | null = null;
|
||||
private readonly extensionOrigin: string;
|
||||
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||
height: "400px",
|
||||
width: "430px",
|
||||
@ -61,6 +63,7 @@ export class OverlayNotificationsContentService
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||
void sendExtensionMessage("checkNotificationQueue");
|
||||
}
|
||||
|
||||
@ -181,7 +184,10 @@ export class OverlayNotificationsContentService
|
||||
this.currentNotificationBarType = initData.type;
|
||||
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
||||
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
||||
this.notificationBarIframeElement.src = chrome.runtime.getURL("notification/bar.html");
|
||||
const parentOrigin = globalThis.location.origin;
|
||||
const iframeUrl = new URL(BrowserApi.getRuntimeURL("notification/bar.html"));
|
||||
iframeUrl.searchParams.set("parentOrigin", parentOrigin);
|
||||
this.notificationBarIframeElement.src = iframeUrl.toString();
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{
|
||||
@ -254,7 +260,11 @@ export class OverlayNotificationsContentService
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMessageToNotificationBarIframe({ command: "initNotificationBar", initData });
|
||||
this.sendMessageToNotificationBarIframe({
|
||||
command: "initNotificationBar",
|
||||
initData,
|
||||
parentOrigin: globalThis.location.origin,
|
||||
});
|
||||
globalThis.removeEventListener("message", handleInitNotificationBarMessage);
|
||||
};
|
||||
|
||||
@ -303,7 +313,7 @@ export class OverlayNotificationsContentService
|
||||
*/
|
||||
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
||||
if (this.notificationBarIframeElement) {
|
||||
this.notificationBarIframeElement.contentWindow.postMessage(message, "*");
|
||||
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,5 +6,5 @@ export interface DomQueryService {
|
||||
mutationObserver?: MutationObserver,
|
||||
forceDeepQueryAttempt?: boolean,
|
||||
): T[];
|
||||
checkPageContainsShadowDom(): boolean;
|
||||
checkPageContainsShadowDom(): void;
|
||||
}
|
||||
|
||||
@ -395,7 +395,7 @@ describe("CollectAutofillContentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the noFieldsFound property to true if the page has no forms or fields", async function () {
|
||||
it("sets the noFieldsFond property to true if the page has no forms or fields", async function () {
|
||||
document.body.innerHTML = "";
|
||||
collectAutofillContentService["noFieldsFound"] = false;
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
||||
@ -2649,33 +2649,4 @@ describe("CollectAutofillContentService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processMutations", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("will require an update to page details if shadow DOM is present", () => {
|
||||
jest
|
||||
.spyOn(domQueryService as any, "checkPageContainsShadowDom")
|
||||
.mockImplementationOnce(() => true);
|
||||
|
||||
collectAutofillContentService["requirePageDetailsUpdate"] = jest.fn();
|
||||
|
||||
collectAutofillContentService["mutationsQueue"] = [[], []];
|
||||
|
||||
collectAutofillContentService["processMutations"]();
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(domQueryService.checkPageContainsShadowDom).toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["mutationsQueue"]).toHaveLength(0);
|
||||
expect(collectAutofillContentService["requirePageDetailsUpdate"]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -997,13 +997,6 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
* within an idle callback to help with performance and prevent excessive updates.
|
||||
*/
|
||||
private processMutations = () => {
|
||||
// If the page contains shadow DOM, we require a page details update from the autofill service.
|
||||
// Will wait for an idle moment on main thread to execute, unless timeout has passed.
|
||||
requestIdleCallbackPolyfill(
|
||||
() => this.domQueryService.checkPageContainsShadowDom() && this.requirePageDetailsUpdate(),
|
||||
{ timeout: 500 },
|
||||
);
|
||||
|
||||
const queueLength = this.mutationsQueue.length;
|
||||
|
||||
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
|
||||
@ -1026,13 +1019,13 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
* Triggers several flags that indicate that a collection of page details should
|
||||
* occur again on a subsequent call after a mutation has been observed in the DOM.
|
||||
*/
|
||||
private requirePageDetailsUpdate = () => {
|
||||
private flagPageDetailsUpdateIsRequired() {
|
||||
this.domRecentlyMutated = true;
|
||||
if (this.autofillOverlayContentService) {
|
||||
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
||||
}
|
||||
this.noFieldsFound = false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all mutation records encountered by the mutation observer.
|
||||
@ -1060,7 +1053,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
|
||||
this.isAutofillElementNodeMutated(mutation.addedNodes))
|
||||
) {
|
||||
this.requirePageDetailsUpdate();
|
||||
this.flagPageDetailsUpdateIsRequired();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -72,6 +72,7 @@ describe("DomQueryService", () => {
|
||||
});
|
||||
|
||||
it("queries form field elements that are nested within multiple ShadowDOM elements", () => {
|
||||
domQueryService["pageContainsShadowDom"] = true;
|
||||
const root = document.createElement("div");
|
||||
const shadowRoot1 = root.attachShadow({ mode: "open" });
|
||||
const root2 = document.createElement("div");
|
||||
@ -94,6 +95,7 @@ describe("DomQueryService", () => {
|
||||
});
|
||||
|
||||
it("will fallback to using the TreeWalker API if a depth larger than 4 ShadowDOM elements is encountered", () => {
|
||||
domQueryService["pageContainsShadowDom"] = true;
|
||||
const root = document.createElement("div");
|
||||
const shadowRoot1 = root.attachShadow({ mode: "open" });
|
||||
const root2 = document.createElement("div");
|
||||
|
||||
@ -78,9 +78,8 @@ export class DomQueryService implements DomQueryServiceInterface {
|
||||
/**
|
||||
* Checks if the page contains any shadow DOM elements.
|
||||
*/
|
||||
checkPageContainsShadowDom = (): boolean => {
|
||||
checkPageContainsShadowDom = (): void => {
|
||||
this.pageContainsShadowDom = this.queryShadowRoots(globalThis.document.body, true).length > 0;
|
||||
return this.pageContainsShadowDom;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -109,7 +108,7 @@ export class DomQueryService implements DomQueryServiceInterface {
|
||||
): T[] {
|
||||
let elements = this.queryElements<T>(root, queryString);
|
||||
|
||||
const shadowRoots = this.pageContainsShadowDom ? this.recursivelyQueryShadowRoots(root) : [];
|
||||
const shadowRoots = this.recursivelyQueryShadowRoots(root);
|
||||
for (let index = 0; index < shadowRoots.length; index++) {
|
||||
const shadowRoot = shadowRoots[index];
|
||||
elements = elements.concat(this.queryElements<T>(shadowRoot, queryString));
|
||||
@ -152,6 +151,10 @@ export class DomQueryService implements DomQueryServiceInterface {
|
||||
root: Document | ShadowRoot | Element,
|
||||
depth: number = 0,
|
||||
): ShadowRoot[] {
|
||||
if (!this.pageContainsShadowDom) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (depth >= MAX_DEEP_QUERY_RECURSION_DEPTH) {
|
||||
throw new Error("Max recursion depth reached");
|
||||
}
|
||||
|
||||
@ -175,6 +175,7 @@ export function createInitAutofillInlineMenuButtonMessageMock(
|
||||
styleSheetUrl: "https://jest-testing-website.com",
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
portKey: "portKey",
|
||||
token: "test-token",
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
@ -212,6 +213,7 @@ export function createInitAutofillInlineMenuListMessageMock(
|
||||
theme: ThemeTypes.Light,
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
portKey: "portKey",
|
||||
token: "test-token",
|
||||
inlineMenuFillType: CipherType.Login,
|
||||
ciphers: [
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
export function triggerTestFailure() {
|
||||
expect(true).toBe("Test has failed.");
|
||||
}
|
||||
@ -11,7 +13,11 @@ export function flushPromises() {
|
||||
});
|
||||
}
|
||||
|
||||
export function postWindowMessage(data: any, origin = "https://localhost/", source = window) {
|
||||
export function postWindowMessage(
|
||||
data: any,
|
||||
origin: string = BrowserApi.getRuntimeURL("")?.slice(0, -1),
|
||||
source: Window | MessageEventSource | null = window,
|
||||
) {
|
||||
globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source }));
|
||||
}
|
||||
|
||||
|
||||
@ -729,7 +729,9 @@ export default class MainBackground {
|
||||
|
||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
||||
this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService);
|
||||
|
||||
@ -865,8 +867,6 @@ export default class MainBackground {
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
this.deviceTrustService = new DeviceTrustService(
|
||||
this.keyGenerationService,
|
||||
@ -882,6 +882,7 @@ export default class MainBackground {
|
||||
this.userDecryptionOptionsService,
|
||||
this.logService,
|
||||
this.configService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.devicesService = new DevicesServiceImplementation(
|
||||
|
||||
@ -293,14 +293,24 @@ export default class RuntimeBackground {
|
||||
case "openPopup":
|
||||
await this.openPopup();
|
||||
break;
|
||||
case VaultMessages.OpenAtRiskPasswords:
|
||||
case VaultMessages.OpenAtRiskPasswords: {
|
||||
if (await this.shouldRejectManyOriginMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.main.openAtRisksPasswordsPage();
|
||||
this.announcePopupOpen();
|
||||
break;
|
||||
case VaultMessages.OpenBrowserExtensionToUrl:
|
||||
}
|
||||
case VaultMessages.OpenBrowserExtensionToUrl: {
|
||||
if (await this.shouldRejectManyOriginMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.main.openTheExtensionToPage(msg.url);
|
||||
this.announcePopupOpen();
|
||||
break;
|
||||
}
|
||||
case "bgUpdateContextMenu":
|
||||
case "editedCipher":
|
||||
case "addedCipher":
|
||||
@ -312,10 +322,7 @@ export default class RuntimeBackground {
|
||||
break;
|
||||
}
|
||||
case "authResult": {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
|
||||
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
|
||||
if (!(await this.isValidVaultReferrer(msg.referrer))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -334,10 +341,7 @@ export default class RuntimeBackground {
|
||||
break;
|
||||
}
|
||||
case "webAuthnResult": {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
|
||||
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
|
||||
if (!(await this.isValidVaultReferrer(msg.referrer))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -372,6 +376,48 @@ export default class RuntimeBackground {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For messages that can originate from a vault host page or extension, validate referrer or external
|
||||
*
|
||||
* @param message
|
||||
* @returns true if message fails validation
|
||||
*/
|
||||
private async shouldRejectManyOriginMessage(message: {
|
||||
webExtSender: chrome.runtime.MessageSender;
|
||||
}): Promise<boolean> {
|
||||
const isValidVaultReferrer = await this.isValidVaultReferrer(
|
||||
Utils.getHostname(message?.webExtSender?.origin),
|
||||
);
|
||||
|
||||
if (isValidVaultReferrer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isExternalMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a message's referrer matches the configured web vault hostname.
|
||||
*
|
||||
* @param referrer - hostname from message source
|
||||
* @returns true if referrer matches web vault
|
||||
*/
|
||||
private async isValidVaultReferrer(referrer: string | null | undefined): Promise<boolean> {
|
||||
if (!referrer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
const vaultHostname = Utils.getHostname(vaultUrl);
|
||||
|
||||
if (!vaultHostname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return vaultHostname === referrer;
|
||||
}
|
||||
|
||||
private async autofillPage(tabToAutoFill: chrome.tabs.Tab) {
|
||||
const totpCode = await this.autofillService.doAutoFill({
|
||||
tab: tabToAutoFill,
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CommonModule } from "@angular/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component, inject } from "@angular/core";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// TODO: This needs to be dealt with by moving this folder or updating the lint rule.
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
@ -1,6 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CommonModule } from "@angular/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@ -56,8 +56,8 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
|
||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
|
||||
import { PhishingWarning } from "../dirt/phishing-detection/pages/phishing-warning.component";
|
||||
import { ProtectedByComponent } from "../dirt/phishing-detection/pages/protected-by-component";
|
||||
import { PhishingWarning } from "../dirt/phishing-detection/popup/phishing-warning.component";
|
||||
import { ProtectedByComponent } from "../dirt/phishing-detection/popup/protected-by-component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
|
||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
@ -92,7 +92,7 @@ import "../platform/popup/locales";
|
||||
TabsV2Component,
|
||||
RemovePasswordComponent,
|
||||
],
|
||||
exports: [],
|
||||
exports: [CalloutModule],
|
||||
providers: [CurrencyPipe, DatePipe],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
||||
@ -36,6 +36,7 @@ import {
|
||||
LoginEmailService,
|
||||
SsoUrlService,
|
||||
LogoutService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@ -609,7 +610,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: Fido2UserVerificationService,
|
||||
useClass: Fido2UserVerificationService,
|
||||
deps: [PasswordRepromptService, UserVerificationService, DialogService],
|
||||
deps: [
|
||||
PasswordRepromptService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
DialogService,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AnimationControlService,
|
||||
|
||||
@ -1,4 +1,19 @@
|
||||
<popup-page>
|
||||
<bit-spotlight *ngIf="!(hasPremium$ | async)" persistent>
|
||||
<span class="tw-text-xs"
|
||||
>{{ "unlockFeaturesWithPremium" | i18n }}
|
||||
<button
|
||||
bitLink
|
||||
buttonType="primary"
|
||||
class="tw-text-xs"
|
||||
type="button"
|
||||
(click)="openUpgradeDialog()"
|
||||
[title]="'upgradeNow' | i18n"
|
||||
>
|
||||
{{ "upgradeNow" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</bit-spotlight>
|
||||
<popup-header slot="header" pageTitle="{{ 'settings' | i18n }}">
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
@ -20,7 +35,7 @@
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<p class="tw-pr-2">{{ "autofill" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="!isBrowserAutofillSettingOverridden && (showAutofillBadge$ | async)"
|
||||
*ngIf="!(isBrowserAutofillSettingOverridden$ | async) && (showAutofillBadge$ | async)"
|
||||
bitBadge
|
||||
variant="notification"
|
||||
[attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||
|
||||
@ -0,0 +1,260 @@
|
||||
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { TestBed, waitForAsync } from "@angular/core/testing";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of, Subject } from "rxjs";
|
||||
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { AutofillBrowserSettingsService } from "@bitwarden/browser/autofill/services/autofill-browser-settings.service";
|
||||
import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/state-test-utils";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
|
||||
import { SettingsV2Component } from "./settings-v2.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-current-account",
|
||||
standalone: true,
|
||||
template: "",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class CurrentAccountStubComponent {}
|
||||
|
||||
describe("SettingsV2Component", () => {
|
||||
let account$: BehaviorSubject<Account | null>;
|
||||
let mockAccountService: Partial<AccountService>;
|
||||
let mockBillingState: { hasPremiumFromAnySource$: jest.Mock };
|
||||
let mockNudges: {
|
||||
showNudgeBadge$: jest.Mock;
|
||||
dismissNudge: jest.Mock;
|
||||
};
|
||||
let mockAutofillSettings: {
|
||||
defaultBrowserAutofillDisabled$: Subject<boolean>;
|
||||
isBrowserAutofillSettingOverridden: jest.Mock<Promise<boolean>>;
|
||||
};
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let openSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(waitForAsync(async () => {
|
||||
dialogService = mock<DialogService>();
|
||||
account$ = new BehaviorSubject<Account | null>(null);
|
||||
mockAccountService = {
|
||||
activeAccount$: account$ as unknown as AccountService["activeAccount$"],
|
||||
};
|
||||
|
||||
mockBillingState = {
|
||||
hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(false)),
|
||||
};
|
||||
|
||||
mockNudges = {
|
||||
showNudgeBadge$: jest.fn().mockImplementation(() => of(false)),
|
||||
dismissNudge: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
mockAutofillSettings = {
|
||||
defaultBrowserAutofillDisabled$: new BehaviorSubject<boolean>(false),
|
||||
isBrowserAutofillSettingOverridden: jest.fn().mockResolvedValue(false),
|
||||
};
|
||||
|
||||
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue("Chrome");
|
||||
|
||||
const cfg = TestBed.configureTestingModule({
|
||||
imports: [SettingsV2Component, RouterTestingModule],
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: BillingAccountProfileStateService, useValue: mockBillingState },
|
||||
{ provide: NudgesService, useValue: mockNudges },
|
||||
{ provide: AutofillBrowserSettingsService, useValue: mockAutofillSettings },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
{ provide: I18nService, useValue: { t: jest.fn((key: string) => key) } },
|
||||
{ provide: GlobalStateProvider, useValue: new FakeGlobalStateProvider() },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: AvatarService, useValue: mock<AvatarService>() },
|
||||
{ provide: AuthService, useValue: mock<AuthService>() },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
});
|
||||
|
||||
TestBed.overrideComponent(SettingsV2Component, {
|
||||
add: {
|
||||
imports: [CurrentAccountStubComponent],
|
||||
providers: [{ provide: DialogService, useValue: dialogService }],
|
||||
},
|
||||
remove: {
|
||||
imports: [CurrentAccountComponent],
|
||||
},
|
||||
});
|
||||
|
||||
await cfg.compileComponents();
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function pushActiveAccount(id = "user-123"): Account {
|
||||
const acct = { id } as Account;
|
||||
account$.next(acct);
|
||||
return acct;
|
||||
}
|
||||
|
||||
it("shows the premium spotlight when user does NOT have premium", async () => {
|
||||
mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
pushActiveAccount();
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
|
||||
expect(el.querySelector("bit-spotlight")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides the premium spotlight when user HAS premium", async () => {
|
||||
mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
pushActiveAccount();
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
expect(el.querySelector("bit-spotlight")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("openUpgradeDialog calls PremiumUpgradeDialogComponent.open with the DialogService", async () => {
|
||||
openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation();
|
||||
mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
pushActiveAccount();
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
component["openUpgradeDialog"]();
|
||||
expect(openSpy).toHaveBeenCalledTimes(1);
|
||||
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 () => {
|
||||
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(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();
|
||||
|
||||
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) => {
|
||||
return of(type === NudgeType.EmptyVaultNudge);
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
await component.dismissBadge(NudgeType.EmptyVaultNudge);
|
||||
|
||||
expect(mockNudges.dismissNudge).toHaveBeenCalledTimes(1);
|
||||
expect(mockNudges.dismissNudge).toHaveBeenCalledWith(NudgeType.EmptyVaultNudge, acct.id, true);
|
||||
});
|
||||
|
||||
it("dismissBadge does nothing when showVaultBadge$ emits false", async () => {
|
||||
pushActiveAccount();
|
||||
|
||||
mockNudges.showNudgeBadge$.mockReturnValue(of(false));
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
await component.dismissBadge(NudgeType.EmptyVaultNudge);
|
||||
|
||||
expect(mockNudges.dismissNudge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("showDownloadBitwardenNudge$ proxies to nudges service for the active account", async () => {
|
||||
const acct = pushActiveAccount("user-xyz");
|
||||
|
||||
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) =>
|
||||
of(type === NudgeType.DownloadBitwarden),
|
||||
);
|
||||
|
||||
const fixture = TestBed.createComponent(SettingsV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const val = await firstValueFrom(component.showDownloadBitwardenNudge$);
|
||||
expect(val).toBe(true);
|
||||
expect(mockNudges.showNudgeBadge$).toHaveBeenCalledWith(NudgeType.DownloadBitwarden, acct.id);
|
||||
});
|
||||
});
|
||||
@ -1,21 +1,31 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BadgeComponent, ItemModule } from "@bitwarden/components";
|
||||
import {
|
||||
BadgeComponent,
|
||||
DialogService,
|
||||
ItemModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||
@ -24,8 +34,6 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "settings-v2.component.html",
|
||||
imports: [
|
||||
@ -38,18 +46,30 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
ItemModule,
|
||||
CurrentAccountComponent,
|
||||
BadgeComponent,
|
||||
SpotlightComponent,
|
||||
TypographyModule,
|
||||
LinkModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SettingsV2Component implements OnInit {
|
||||
export class SettingsV2Component {
|
||||
NudgeType = NudgeType;
|
||||
activeUserId: UserId | null = null;
|
||||
protected isBrowserAutofillSettingOverridden = false;
|
||||
|
||||
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 }),
|
||||
);
|
||||
|
||||
protected hasPremium$ = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) => this.accountProfileStateService.hasPremiumFromAnySource$(account.id)),
|
||||
);
|
||||
|
||||
showDownloadBitwardenNudge$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
|
||||
@ -79,13 +99,12 @@ export class SettingsV2Component implements OnInit {
|
||||
private readonly nudgesService: NudgesService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||
private readonly accountProfileStateService: BillingAccountProfileStateService,
|
||||
private readonly dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isBrowserAutofillSettingOverridden =
|
||||
await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
|
||||
BrowserApi.getBrowserClientVendor(window),
|
||||
);
|
||||
protected openUpgradeDialog() {
|
||||
PremiumUpgradeDialogComponent.open(this.dialogService);
|
||||
}
|
||||
|
||||
async dismissBadge(type: NudgeType) {
|
||||
|
||||
@ -53,12 +53,14 @@
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
<div slot="default-trailing">
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
</div>
|
||||
</button>
|
||||
<bit-item-action slot="end">
|
||||
<button
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
class="tw-text-sm tw-font-medium tw-cursor-pointer"
|
||||
(click)="toggleSavedUrlExpandedState()"
|
||||
>
|
||||
{{ (savedUrlsExpanded() ? "viewLess" : "viewAll") | i18n }}
|
||||
{{ (savedUrlsExpanded() ? "showLess" : "showAll") | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-pt-2" [ngClass]="savedUrlsListClass()">
|
||||
|
||||
@ -91,6 +91,11 @@ describe("AutofillConfirmationDialogComponent", () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const findShowAll = (inFx?: ComponentFixture<AutofillConfirmationDialogComponent>) =>
|
||||
(inFx || fixture).nativeElement.querySelector(
|
||||
"button.tw-text-sm.tw-font-medium.tw-cursor-pointer",
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
it("normalizes currentUrl and savedUrls via Utils.getHostname", () => {
|
||||
expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0));
|
||||
expect(component.currentUrl()).toBe("example.com");
|
||||
@ -191,21 +196,47 @@ describe("AutofillConfirmationDialogComponent", () => {
|
||||
expect(text).toContain("two.example.com");
|
||||
});
|
||||
|
||||
it("shows the 'view all' button when savedUrls > 1 and toggles the button text when clicked", () => {
|
||||
const findViewAll = () =>
|
||||
fixture.nativeElement.querySelector(
|
||||
"button.tw-text-sm.tw-font-medium.tw-cursor-pointer",
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
let btn = findViewAll();
|
||||
it("shows the 'show all' button when savedUrls > 1", () => {
|
||||
const btn = findShowAll();
|
||||
expect(btn).toBeTruthy();
|
||||
expect(btn!.textContent).toContain("showAll");
|
||||
});
|
||||
|
||||
it('hides the "show all" button when savedUrls is empty', async () => {
|
||||
const newParams: AutofillConfirmationDialogParams = {
|
||||
currentUrl: "https://bitwarden.com/help",
|
||||
savedUrls: [],
|
||||
};
|
||||
|
||||
const { fixture: vf } = await createFreshFixture({ params: newParams });
|
||||
vf.detectChanges();
|
||||
const btn = findShowAll(vf);
|
||||
expect(btn).toBeNull();
|
||||
});
|
||||
|
||||
it("handles toggling of the 'show all' button correctly", async () => {
|
||||
const { fixture: vf, component: vc } = await createFreshFixture();
|
||||
|
||||
let btn = findShowAll(vf);
|
||||
expect(btn).toBeTruthy();
|
||||
expect(vc.savedUrlsExpanded()).toBe(false);
|
||||
expect(btn!.textContent).toContain("showAll");
|
||||
|
||||
// click to expand
|
||||
btn!.click();
|
||||
fixture.detectChanges();
|
||||
vf.detectChanges();
|
||||
|
||||
btn = findViewAll();
|
||||
expect(btn!.textContent).toContain("viewLess");
|
||||
expect(component.savedUrlsExpanded()).toBe(true);
|
||||
btn = findShowAll(vf);
|
||||
expect(btn!.textContent).toContain("showLess");
|
||||
expect(vc.savedUrlsExpanded()).toBe(true);
|
||||
|
||||
// click to collapse
|
||||
btn!.click();
|
||||
vf.detectChanges();
|
||||
|
||||
btn = findShowAll(vf);
|
||||
expect(btn!.textContent).toContain("showAll");
|
||||
expect(vc.savedUrlsExpanded()).toBe(false);
|
||||
});
|
||||
|
||||
it("shows autofillWithoutAdding text on autofill button when viewOnly is false", () => {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
|
||||
import { CopyableCipherFields } from "@bitwarden/sdk-internal";
|
||||
import { CopyAction, CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
import { CopyFieldAction, CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
|
||||
|
||||
@ -18,7 +18,7 @@ type CipherItem = {
|
||||
/** Translation key for the respective value */
|
||||
key: string;
|
||||
/** Property key on `CipherView` to retrieve the copy value */
|
||||
field: CopyAction;
|
||||
field: CopyFieldAction;
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@ -48,7 +48,7 @@ export class ItemCopyActionsComponent {
|
||||
* singleCopyableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
|
||||
* code to be copied correctly. See #14167
|
||||
*/
|
||||
get singleCopyableLogin() {
|
||||
get singleCopyableLogin(): CipherItem | null {
|
||||
const loginItems: CipherItem[] = [
|
||||
{ key: "copyUsername", field: "username" },
|
||||
{ key: "copyPassword", field: "password" },
|
||||
@ -62,7 +62,7 @@ export class ItemCopyActionsComponent {
|
||||
) {
|
||||
return {
|
||||
key: this.i18nService.t("copyUsername"),
|
||||
field: "username",
|
||||
field: "username" as const,
|
||||
};
|
||||
}
|
||||
return this.findSingleCopyableItem(loginItems);
|
||||
|
||||
@ -51,10 +51,26 @@
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
@if (canArchive$ | async) {
|
||||
<button type="button" bitMenuItem (click)="archive()">
|
||||
{{ "archiveVerb" | i18n }}
|
||||
</button>
|
||||
@if (showArchive$ | async) {
|
||||
@if (canArchive$ | async) {
|
||||
<button type="button" bitMenuItem (click)="archive()">
|
||||
{{ "archiveVerb" | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="badge.promptForPremium($event)"
|
||||
[attr.aria-label]="'upgradeToUseArchive' | i18n"
|
||||
>
|
||||
<div class="tw-flex tw-flex-nowrap tw-items-center tw-gap-2">
|
||||
{{ "archiveVerb" | i18n }}
|
||||
<div aria-hidden>
|
||||
<app-premium-badge #badge></app-premium-badge>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (canDelete$ | async) {
|
||||
<button type="button" bitMenuItem (click)="delete()">
|
||||
|
||||
@ -106,7 +106,10 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
},
|
||||
{ provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } },
|
||||
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
||||
{ provide: CipherArchiveService, useValue: { userCanArchive$: () => of(true) } },
|
||||
{
|
||||
provide: CipherArchiveService,
|
||||
useValue: { userCanArchive$: () => of(true), hasArchiveFlagEnabled$: () => of(true) },
|
||||
},
|
||||
{ provide: ToastService, useValue: { showToast: () => {} } },
|
||||
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
||||
{ provide: PasswordRepromptService, useValue: passwordRepromptService },
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@ -17,6 +18,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
@ -33,6 +35,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
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";
|
||||
@ -46,7 +49,18 @@ import {
|
||||
@Component({
|
||||
selector: "app-item-more-options",
|
||||
templateUrl: "./item-more-options.component.html",
|
||||
imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
|
||||
imports: [
|
||||
ItemModule,
|
||||
IconButtonModule,
|
||||
MenuModule,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
RouterModule,
|
||||
PremiumBadgeComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
],
|
||||
})
|
||||
export class ItemMoreOptionsComponent {
|
||||
private _cipher$ = new BehaviorSubject<CipherViewLike>({} as CipherViewLike);
|
||||
@ -127,18 +141,11 @@ export class ItemMoreOptionsComponent {
|
||||
}),
|
||||
);
|
||||
|
||||
/** Observable Boolean checking if item can show Archive menu option */
|
||||
protected canArchive$ = combineLatest([
|
||||
this._cipher$,
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
),
|
||||
]).pipe(
|
||||
filter(([cipher, userId]) => cipher != null && userId != null),
|
||||
map(([cipher, canArchive]) => {
|
||||
return canArchive && !CipherViewLikeUtils.isArchived(cipher) && cipher.organizationId == null;
|
||||
}),
|
||||
protected showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$();
|
||||
|
||||
protected canArchive$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
);
|
||||
|
||||
protected canDelete$ = this._cipher$.pipe(
|
||||
@ -377,6 +384,11 @@ export class ItemMoreOptionsComponent {
|
||||
}
|
||||
|
||||
async archive() {
|
||||
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher);
|
||||
if (!repromptPassed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "archiveItem" },
|
||||
content: { key: "archiveItemConfirmDesc" },
|
||||
|
||||
@ -44,8 +44,8 @@
|
||||
<ng-container slot="above-scroll-area">
|
||||
<ng-container *ngIf="showPremiumSpotlight$ | async">
|
||||
<bit-spotlight
|
||||
[title]="'upgradeCompleteSecurity' | i18n"
|
||||
[subtitle]="'premiumGivesMoreTools' | i18n"
|
||||
[title]="'unlockAdvancedSecurity' | i18n"
|
||||
[subtitle]="'unlockAdvancedSecurityDesc' | i18n"
|
||||
[buttonText]="'explorePremium' | i18n"
|
||||
(onButtonClick)="showPremiumDialog()"
|
||||
(onDismiss)="dismissVaultNudgeSpotlight(NudgeType.PremiumUpgrade)"
|
||||
|
||||
@ -165,15 +165,14 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected showPremiumSpotlight$ = combineLatest([
|
||||
this.showPremiumNudgeSpotlight$,
|
||||
this.showEmptyVaultSpotlight$,
|
||||
this.showHasItemsVaultSpotlight$,
|
||||
this.hasPremium$,
|
||||
this.cipherCount$,
|
||||
this.accountAgeInDays$,
|
||||
]).pipe(
|
||||
map(
|
||||
([showNudge, emptyVault, hasItems, hasPremium, count, age]) =>
|
||||
showNudge && !emptyVault && !hasItems && !hasPremium && count >= 5 && age >= 7,
|
||||
([showPremiumNudge, showHasItemsNudge, hasPremium, count, age]) =>
|
||||
showPremiumNudge && !showHasItemsNudge && !hasPremium && count >= 5 && age >= 7,
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<popup-page [loading]="loading$ | async">
|
||||
<popup-header slot="header" [pageTitle]="'archive' | i18n" showBackButton>
|
||||
<popup-header slot="header" [pageTitle]="'archiveNoun' | i18n" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
@ -27,10 +27,10 @@
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
@if (cipher.hasAttachments) {
|
||||
@if (CipherViewLikeUtils.hasAttachments(cipher)) {
|
||||
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
|
||||
}
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
<span slot="secondary">{{ CipherViewLikeUtils.subtitle(cipher) }}</span>
|
||||
</button>
|
||||
<bit-item-action slot="end">
|
||||
<button
|
||||
|
||||
@ -11,7 +11,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import {
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
@ -71,12 +74,14 @@ export class ArchiveComponent {
|
||||
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
|
||||
);
|
||||
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
protected loading$ = this.archivedCiphers$.pipe(
|
||||
map(() => false),
|
||||
startWith(true),
|
||||
);
|
||||
|
||||
async view(cipher: CipherView) {
|
||||
async view(cipher: CipherViewLike) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
@ -86,7 +91,7 @@ export class ArchiveComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async edit(cipher: CipherView) {
|
||||
async edit(cipher: CipherViewLike) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
@ -96,7 +101,7 @@ export class ArchiveComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async delete(cipher: CipherView) {
|
||||
async delete(cipher: CipherViewLike) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
@ -113,7 +118,7 @@ export class ArchiveComponent {
|
||||
const activeUserId = await firstValueFrom(this.userId$);
|
||||
|
||||
try {
|
||||
await this.cipherService.softDeleteWithServer(cipher.id, activeUserId);
|
||||
await this.cipherService.softDeleteWithServer(cipher.id as string, activeUserId);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
return;
|
||||
@ -125,13 +130,16 @@ export class ArchiveComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async unarchive(cipher: CipherView) {
|
||||
async unarchive(cipher: CipherViewLike) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
const activeUserId = await firstValueFrom(this.userId$);
|
||||
|
||||
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
|
||||
await this.cipherArchiveService.unarchiveWithServer(
|
||||
cipher.id as unknown as CipherId,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@ -139,12 +147,12 @@ export class ArchiveComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async clone(cipher: CipherView) {
|
||||
async clone(cipher: CipherViewLike) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipher.login?.hasFido2Credentials) {
|
||||
if (CipherViewLikeUtils.hasFido2Credentials(cipher)) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
@ -171,8 +179,8 @@ export class ArchiveComponent {
|
||||
* @param cipher
|
||||
* @private
|
||||
*/
|
||||
private canInteract(cipher: CipherView) {
|
||||
if (cipher.decryptionFailure) {
|
||||
private canInteract(cipher: CipherViewLike) {
|
||||
if (CipherViewLikeUtils.decryptionFailure(cipher)) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
|
||||
@ -6,12 +6,6 @@
|
||||
</popup-header>
|
||||
|
||||
<bit-item-group>
|
||||
<bit-item *ngIf="!(canAccessPremium$ | async)">
|
||||
<a type="button" bit-item-content routerLink="/premium">
|
||||
{{ "premiumMembership" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item
|
||||
*ngIf="
|
||||
(familySponsorshipAvailable$ | async) &&
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Observable, firstValueFrom, of, switchMap } from "rxjs";
|
||||
import { Observable, firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { DialogService, ItemModule } from "@bitwarden/components";
|
||||
|
||||
@ -32,14 +31,12 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
],
|
||||
})
|
||||
export class MoreFromBitwardenPageV2Component {
|
||||
canAccessPremium$: Observable<boolean>;
|
||||
protected familySponsorshipAvailable$: Observable<boolean>;
|
||||
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
|
||||
protected hasSingleEnterpriseOrg$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private environmentService: EnvironmentService,
|
||||
private organizationService: OrganizationService,
|
||||
private familiesPolicyService: FamiliesPolicyService,
|
||||
@ -48,13 +45,6 @@ export class MoreFromBitwardenPageV2Component {
|
||||
this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe(
|
||||
switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)),
|
||||
);
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
|
||||
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
|
||||
}
|
||||
|
||||
@ -25,11 +25,11 @@
|
||||
[appA11yTitle]="orgIconTooltip(cipher)"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
*ngIf="hasAttachments(cipher)"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
<span slot="secondary">{{ getSubtitle(cipher) }}</span>
|
||||
</button>
|
||||
<ng-container slot="end" *ngIf="cipher.permissions.restore">
|
||||
<bit-item-action>
|
||||
@ -45,7 +45,7 @@
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="restore(cipher)"
|
||||
*ngIf="!cipher.decryptionFailure"
|
||||
*ngIf="!hasDecryptionFailure(cipher)"
|
||||
>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
|
||||
@ -12,7 +12,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
@ -85,10 +84,40 @@ export class TrashListItemsContainerComponent {
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
async restore(cipher: CipherView) {
|
||||
/**
|
||||
* Check if a cipher has attachments. CipherView has a hasAttachments getter,
|
||||
* while CipherListView has an attachments count property.
|
||||
*/
|
||||
hasAttachments(cipher: PopupCipherViewLike): boolean {
|
||||
if ("hasAttachments" in cipher) {
|
||||
return cipher.hasAttachments;
|
||||
}
|
||||
return cipher.attachments > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subtitle for a cipher. CipherView has a subTitle getter,
|
||||
* while CipherListView has a subtitle property.
|
||||
*/
|
||||
getSubtitle(cipher: PopupCipherViewLike): string | undefined {
|
||||
if ("subTitle" in cipher) {
|
||||
return cipher.subTitle;
|
||||
}
|
||||
return cipher.subtitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cipher has a decryption failure. CipherView has this property,
|
||||
* while CipherListView does not.
|
||||
*/
|
||||
hasDecryptionFailure(cipher: PopupCipherViewLike): boolean {
|
||||
return "decryptionFailure" in cipher && cipher.decryptionFailure;
|
||||
}
|
||||
|
||||
async restore(cipher: PopupCipherViewLike) {
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.restoreWithServer(cipher.id, activeUserId);
|
||||
await this.cipherService.restoreWithServer(cipher.id as string, activeUserId);
|
||||
|
||||
await this.router.navigate(["/trash"]);
|
||||
this.toastService.showToast({
|
||||
@ -101,7 +130,7 @@ export class TrashListItemsContainerComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async delete(cipher: CipherView) {
|
||||
async delete(cipher: PopupCipherViewLike) {
|
||||
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||
|
||||
if (!repromptPassed) {
|
||||
@ -120,7 +149,7 @@ export class TrashListItemsContainerComponent {
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.deleteWithServer(cipher.id, activeUserId);
|
||||
await this.cipherService.deleteWithServer(cipher.id as string, activeUserId);
|
||||
|
||||
await this.router.navigate(["/trash"]);
|
||||
this.toastService.showToast({
|
||||
@ -133,8 +162,9 @@ export class TrashListItemsContainerComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async onViewCipher(cipher: CipherView) {
|
||||
if (cipher.decryptionFailure) {
|
||||
async onViewCipher(cipher: PopupCipherViewLike) {
|
||||
// CipherListView doesn't have decryptionFailure, so we use optional chaining
|
||||
if ("decryptionFailure" in cipher && cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
@ -147,7 +177,7 @@ export class TrashListItemsContainerComponent {
|
||||
}
|
||||
|
||||
await this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||
queryParams: { cipherId: cipher.id as string, type: cipher.type },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,13 +34,27 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
@if (userCanArchive() || showArchiveFilter()) {
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/archive">
|
||||
{{ "archiveNoun" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
@if (showArchiveItem()) {
|
||||
@if (userCanArchive()) {
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/archive">
|
||||
{{ "archiveNoun" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
} @else {
|
||||
<bit-item>
|
||||
<a bit-item-content [routerLink]="userHasArchivedItems() ? '/archive' : '/premium'">
|
||||
<span class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "archiveNoun" | i18n }}
|
||||
@if (!userHasArchivedItems()) {
|
||||
<app-premium-badge></app-premium-badge>
|
||||
}
|
||||
</span>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
}
|
||||
}
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/trash">
|
||||
|
||||
@ -2,14 +2,16 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
|
||||
@ -18,6 +20,7 @@ import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
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";
|
||||
import { BrowserPremiumUpgradePromptService } from "../services/browser-premium-upgrade-prompt.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@ -32,20 +35,28 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
PopOutComponent,
|
||||
ItemModule,
|
||||
BadgeComponent,
|
||||
PremiumBadgeComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
],
|
||||
})
|
||||
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||
lastSync = "--";
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
// Check if user is premium user, they will be able to archive items
|
||||
protected readonly userCanArchive = toSignal(
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||
);
|
||||
|
||||
// Check if user has archived items (does not check if user is premium)
|
||||
protected readonly showArchiveFilter = toSignal(
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
|
||||
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
||||
|
||||
protected readonly userHasArchivedItems = toSignal(
|
||||
this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((c) => c.length > 0)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME (PM-22628): Popup imports are forbidden in background
|
||||
@ -31,21 +35,24 @@ describe("Fido2UserVerificationService", () => {
|
||||
let fido2UserVerificationService: Fido2UserVerificationService;
|
||||
|
||||
let passwordRepromptService: MockProxy<PasswordRepromptService>;
|
||||
let userVerificationService: MockProxy<UserVerificationService>;
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let accountService: FakeAccountService;
|
||||
let cipher: CipherView;
|
||||
|
||||
beforeEach(() => {
|
||||
passwordRepromptService = mock<PasswordRepromptService>();
|
||||
userVerificationService = mock<UserVerificationService>();
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
dialogService = mock<DialogService>();
|
||||
accountService = mockAccountServiceWith(newGuid() as UserId);
|
||||
|
||||
cipher = createCipherView();
|
||||
|
||||
fido2UserVerificationService = new Fido2UserVerificationService(
|
||||
passwordRepromptService,
|
||||
userVerificationService,
|
||||
userDecryptionOptionsService,
|
||||
dialogService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
|
||||
@ -67,7 +74,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
@ -82,7 +89,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
true,
|
||||
@ -98,7 +105,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
true,
|
||||
@ -114,7 +121,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
@ -176,7 +183,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
@ -191,7 +198,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
false,
|
||||
@ -207,7 +214,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
false,
|
||||
@ -223,7 +230,7 @@ describe("Fido2UserVerificationService", () => {
|
||||
|
||||
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||
|
||||
const result = await fido2UserVerificationService.handleUserVerification(
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
@ -15,8 +16,9 @@ import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
||||
export class Fido2UserVerificationService {
|
||||
constructor(
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -78,7 +80,15 @@ export class Fido2UserVerificationService {
|
||||
}
|
||||
|
||||
private async handleMasterPasswordReprompt(): Promise<boolean> {
|
||||
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
if (!activeAccount?.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasMasterPassword = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.hasMasterPasswordById$(activeAccount.id),
|
||||
);
|
||||
|
||||
// TDE users have no master password, so we need to use the UserVerification prompt
|
||||
return hasMasterPassword
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base",
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"../../libs/common/src/autofill/constants",
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.7.3",
|
||||
"tldts": "7.0.1",
|
||||
"tldts": "7.0.18",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -513,7 +513,9 @@ export class ServiceContainer {
|
||||
")";
|
||||
|
||||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
this.ssoUrlService = new SsoUrlService();
|
||||
|
||||
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
||||
@ -706,6 +708,7 @@ export class ServiceContainer {
|
||||
this.userDecryptionOptionsService,
|
||||
this.logService,
|
||||
this.configService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.loginStrategyService = new LoginStrategyService(
|
||||
|
||||
5
apps/desktop/desktop_native/Cargo.lock
generated
5
apps/desktop/desktop_native/Cargo.lock
generated
@ -120,9 +120,9 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227"
|
||||
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"log",
|
||||
@ -131,6 +131,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
@ -22,7 +22,7 @@ publish = false
|
||||
aes = "=0.8.4"
|
||||
aes-gcm = "=0.10.3"
|
||||
anyhow = "=1.0.94"
|
||||
arboard = { version = "=3.6.0", default-features = false }
|
||||
arboard = { version = "=3.6.1", default-features = false }
|
||||
ashpd = "=0.11.0"
|
||||
base64 = "=0.22.1"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.11.2",
|
||||
"version": "2025.11.3",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@ -39,7 +39,7 @@
|
||||
"clean:dist": "rimraf ./dist",
|
||||
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",
|
||||
"pack:lin:flatpak": "flatpak-builder --repo=../../.flatpak-repo ../../.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ../../.flatpak-repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && tar -czvf ./dist/bitwarden_desktop_x64.tar.gz -C ./dist/linux-unpacked/ .",
|
||||
"pack:lin:arm64": "npm run clean:dist && electron-builder --linux --arm64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .",
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
|
||||
"pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never",
|
||||
|
||||
@ -119,7 +119,9 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
|
||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
userDecryptionOptionsSubject,
|
||||
);
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
credentials.newServerMasterKeyHash,
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.11.2",
|
||||
"version": "2025.11.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.11.2",
|
||||
"version": "2025.11.3",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.11.2",
|
||||
"version": "2025.11.3",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@ -30,20 +30,23 @@
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
*ngIf="!hideArchive"
|
||||
class="filter-option tw-flex tw-items-center tw-gap-2 [&>span]:tw-w-min"
|
||||
[ngClass]="{ active: activeFilter.status === 'archive' }"
|
||||
*ngIf="!hideArchive"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter('archive')"
|
||||
(click)="handleArchiveFilter($event)"
|
||||
[attr.aria-pressed]="activeFilter.status === 'archive'"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i> {{ "archiveNoun" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
@if (!(canArchive$ | async)) {
|
||||
<app-premium-badge></app-premium-badge>
|
||||
}
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { StatusFilterComponent } from "./status-filter.component";
|
||||
|
||||
describe("StatusFilterComponent", () => {
|
||||
let component: StatusFilterComponent;
|
||||
let fixture: ComponentFixture<StatusFilterComponent>;
|
||||
let cipherArchiveService: jest.Mocked<CipherArchiveService>;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const event = new Event("click");
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
cipherArchiveService = mock<CipherArchiveService>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [StatusFilterComponent],
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: CipherArchiveService, useValue: cipherArchiveService },
|
||||
{ provide: PremiumUpgradePromptService, useValue: mock<PremiumUpgradePromptService>() },
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: mock<BillingAccountProfileStateService>(),
|
||||
},
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
imports: [JslibModule, PremiumBadgeComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(StatusFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.activeFilter = new VaultFilter();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("handleArchiveFilter", () => {
|
||||
const applyFilter = jest.fn();
|
||||
let promptForPremiumSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
applyFilter.mockClear();
|
||||
component["applyFilter"] = applyFilter;
|
||||
|
||||
promptForPremiumSpy = jest.spyOn(component["premiumBadgeComponent"]()!, "promptForPremium");
|
||||
});
|
||||
|
||||
it("should apply archive filter when userCanArchive returns true", async () => {
|
||||
cipherArchiveService.userCanArchive$.mockReturnValue(of(true));
|
||||
cipherArchiveService.archivedCiphers$.mockReturnValue(of([]));
|
||||
|
||||
await component["handleArchiveFilter"](event);
|
||||
|
||||
expect(applyFilter).toHaveBeenCalledWith("archive");
|
||||
expect(promptForPremiumSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should apply archive filter when userCanArchive returns false but hasArchivedCiphers is true", async () => {
|
||||
const mockCipher = new CipherView();
|
||||
mockCipher.id = "test-id";
|
||||
|
||||
cipherArchiveService.userCanArchive$.mockReturnValue(of(false));
|
||||
cipherArchiveService.archivedCiphers$.mockReturnValue(of([mockCipher]));
|
||||
|
||||
await component["handleArchiveFilter"](event);
|
||||
|
||||
expect(applyFilter).toHaveBeenCalledWith("archive");
|
||||
expect(promptForPremiumSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should prompt for premium when userCanArchive returns false and hasArchivedCiphers is false", async () => {
|
||||
cipherArchiveService.userCanArchive$.mockReturnValue(of(false));
|
||||
cipherArchiveService.archivedCiphers$.mockReturnValue(of([]));
|
||||
|
||||
await component["handleArchiveFilter"](event);
|
||||
|
||||
expect(applyFilter).not.toHaveBeenCalled();
|
||||
expect(promptForPremiumSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,11 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, viewChild } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/status-filter.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@ -9,4 +14,38 @@ import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/a
|
||||
templateUrl: "status-filter.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class StatusFilterComponent extends BaseStatusFilterComponent {}
|
||||
export class StatusFilterComponent extends BaseStatusFilterComponent {
|
||||
private readonly premiumBadgeComponent = viewChild(PremiumBadgeComponent);
|
||||
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
protected canArchive$ = this.userId$.pipe(
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
);
|
||||
|
||||
protected hasArchivedCiphers$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async handleArchiveFilter(event: Event) {
|
||||
const [canArchive, hasArchivedCiphers] = await firstValueFrom(
|
||||
combineLatest([this.canArchive$, this.hasArchivedCiphers$]),
|
||||
);
|
||||
|
||||
if (canArchive || hasArchivedCiphers) {
|
||||
this.applyFilter("archive");
|
||||
} else if (this.premiumBadgeComponent()) {
|
||||
// The `premiumBadgeComponent` should always be defined here, adding the
|
||||
// if to satisfy TypeScript.
|
||||
await this.premiumBadgeComponent().promptForPremium(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/vault/abstractions/deprecated-vault-filter.service";
|
||||
import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
@ -13,7 +14,7 @@ import { TypeFilterComponent } from "./filters/type-filter.component";
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, JslibModule],
|
||||
imports: [CommonModule, JslibModule, PremiumBadgeComponent],
|
||||
declarations: [
|
||||
VaultFilterComponent,
|
||||
CollectionFilterComponent,
|
||||
|
||||
@ -565,10 +565,15 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
}
|
||||
}
|
||||
|
||||
if (userCanArchive && !cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) {
|
||||
if (!cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("archiveVerb"),
|
||||
click: async () => {
|
||||
if (!userCanArchive) {
|
||||
await this.premiumUpgradePromptService.promptForPremium();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.archiveCipherUtilitiesService.archiveCipher(cipher);
|
||||
await this.refreshCurrentCipher();
|
||||
},
|
||||
|
||||
@ -4,5 +4,6 @@
|
||||
"isolatedModules": true,
|
||||
"emitDecoratorMetadata": false
|
||||
},
|
||||
"files": ["./test.setup.ts"]
|
||||
"files": ["./test.setup.ts"],
|
||||
"include": ["src/**/*.spec.ts"]
|
||||
}
|
||||
|
||||
@ -154,45 +154,15 @@
|
||||
},
|
||||
"configurations": {
|
||||
"oss": {
|
||||
"buildTarget": "web:build:oss"
|
||||
},
|
||||
"oss-dev": {
|
||||
"buildTarget": "web:build:oss-dev"
|
||||
},
|
||||
"commercial": {
|
||||
"buildTarget": "web:build:commercial"
|
||||
},
|
||||
"commercial-dev": {
|
||||
"buildTarget": "web:build:commercial-dev"
|
||||
},
|
||||
"commercial-qa": {
|
||||
"buildTarget": "web:build:commercial-qa"
|
||||
},
|
||||
"commercial-cloud": {
|
||||
"buildTarget": "web:build:commercial-cloud"
|
||||
},
|
||||
"commercial-euprd": {
|
||||
"buildTarget": "web:build:commercial-euprd"
|
||||
},
|
||||
"commercial-euqa": {
|
||||
"buildTarget": "web:build:commercial-euqa"
|
||||
},
|
||||
"commercial-usdev": {
|
||||
"buildTarget": "web:build:commercial-usdev"
|
||||
},
|
||||
"commercial-ee": {
|
||||
"buildTarget": "web:build:commercial-ee"
|
||||
},
|
||||
"oss-selfhost": {
|
||||
"buildTarget": "web:build:oss-selfhost"
|
||||
},
|
||||
"oss-selfhost-dev": {
|
||||
"buildTarget": "web:build:oss-selfhost-dev"
|
||||
},
|
||||
"commercial-selfhost": {
|
||||
"buildTarget": "web:build:commercial-selfhost"
|
||||
},
|
||||
"commercial-selfhost-dev": {
|
||||
"buildTarget": "web:build:commercial-selfhost-dev"
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,18 +168,11 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new OrganizationUpdateRequest();
|
||||
|
||||
/*
|
||||
* When you disable a FormControl, it is removed from formGroup.values, so we have to use
|
||||
* the original value.
|
||||
* */
|
||||
request.name = this.formGroup.get("orgName").disabled
|
||||
? this.org.name
|
||||
: this.formGroup.value.orgName;
|
||||
request.billingEmail = this.formGroup.get("billingEmail").disabled
|
||||
? this.org.billingEmail
|
||||
: this.formGroup.value.billingEmail;
|
||||
// The server ignores any undefined values, so it's ok to reference disabled form fields here
|
||||
const request: OrganizationUpdateRequest = {
|
||||
name: this.formGroup.value.orgName,
|
||||
billingEmail: this.formGroup.value.billingEmail,
|
||||
};
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
if (!this.org.hasPublicAndPrivateKeys) {
|
||||
|
||||
@ -123,7 +123,9 @@ describe("WebSetInitialPasswordService", () => {
|
||||
|
||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
userDecryptionOptionsSubject,
|
||||
);
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
credentials.newServerMasterKeyHash,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user