1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 05:53:42 +00:00

Merge branch 'main' into km/disable-legacy-ciphers

This commit is contained in:
Bernd Schoolmann
2025-01-06 16:47:48 +01:00
committed by GitHub
235 changed files with 6816 additions and 5050 deletions

View File

@@ -114,8 +114,8 @@ jobs:
fi
build:
name: Build
build-source:
name: Build browser source
runs-on: ubuntu-22.04
needs:
- setup
@@ -127,7 +127,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@@ -169,21 +169,91 @@ jobs:
zip -r browser-source.zip browser-source
- name: Upload browser source
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: browser-source-${{ env._BUILD_NUMBER }}.zip
path: browser-source.zip
if-no-files-found: error
build:
name: Build
runs-on: ubuntu-22.04
needs:
- setup
- locales-test
- build-source
env:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
strategy:
matrix:
include:
- name: "chrome"
npm_command: "dist:chrome"
archive_name: "dist-chrome.zip"
artifact_name: "dist-chrome-MV3"
- name: "edge"
npm_command: "dist:edge"
archive_name: "dist-edge.zip"
artifact_name: "dist-edge"
- name: "edge-mv3"
npm_command: "dist:edge:mv3"
archive_name: "dist-edge.zip"
artifact_name: "DO-NOT-USE-FOR-PROD-dist-edge-MV3"
- name: "firefox"
npm_command: "dist:firefox"
archive_name: "dist-firefox.zip"
artifact_name: "dist-firefox"
- name: "firefox-mv3"
npm_command: "dist:firefox:mv3"
archive_name: "dist-firefox.zip"
artifact_name: "DO-NOT-USE-FOR-PROD-dist-firefox-MV3"
- name: "opera"
npm_command: "dist:opera"
archive_name: "dist-opera.zip"
artifact_name: "dist-opera"
- name: "opera-mv3"
npm_command: "dist:opera:mv3"
archive_name: "dist-opera.zip"
artifact_name: "DO-NOT-USE-FOR-PROD-dist-opera-MV3"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Print environment
run: |
node --version
npm --version
- name: Download browser source
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: browser-source-${{ env._BUILD_NUMBER }}.zip
- name: Unzip browser source artifact
run: |
unzip browser-source.zip
rm browser-source.zip
- name: NPM setup
run: npm ci
working-directory: browser-source/
- name: Download SDK Artifacts
- name: Download SDK artifacts
if: ${{ inputs.sdk_branch != '' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: build-wasm-internal.yml
workflow_conclusion: success
branch: ${{ inputs.sdk_branch }}
@@ -195,85 +265,19 @@ jobs:
- name: Override SDK
if: ${{ inputs.sdk_branch != '' }}
working-directory: browser-source/
run: |
npm link ../sdk-internal
run: npm link ../sdk-internal
- name: Build Chrome
run: npm run dist:chrome
- name: Build extension
run: npm run ${{ matrix.npm_command }}
working-directory: browser-source/apps/browser
- name: Upload Chrome MV3 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
- name: Upload extension artifact
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome.zip
name: ${{ matrix.artifact_name }}-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/${{ matrix.archive_name }}
if-no-files-found: error
- name: Build Edge
run: npm run dist:edge
working-directory: browser-source/apps/browser
- name: Upload Edge artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist-edge-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-edge.zip
if-no-files-found: error
- name: Build Edge (MV3)
run: npm run dist:edge:mv3
working-directory: browser-source/apps/browser
- name: Upload Edge MV3 artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-edge.zip
if-no-files-found: error
- name: Build Firefox
run: npm run dist:firefox
working-directory: browser-source/apps/browser
- name: Upload Firefox artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist-firefox-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-firefox.zip
if-no-files-found: error
- name: Build Firefox (MV3)
run: npm run dist:firefox:mv3
working-directory: browser-source/apps/browser
- name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-firefox.zip
if-no-files-found: error
- name: Build Opera
run: npm run dist:opera
working-directory: browser-source/apps/browser
- name: Upload Opera artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist-opera-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-opera.zip
if-no-files-found: error
- name: Build Opera (MV3)
run: npm run dist:opera:mv3
working-directory: browser-source/apps/browser
- name: Upload Opera MV3 artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-opera.zip
if-no-files-found: error
build-safari:
name: Build Safari
@@ -405,7 +409,7 @@ jobs:
ls -la
- name: Upload Safari artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: dist-safari-${{ env._BUILD_NUMBER }}.zip
path: apps/browser/dist/dist-safari.zip
@@ -448,6 +452,7 @@ jobs:
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()
@@ -455,6 +460,7 @@ jobs:
needs:
- setup
- locales-test
- build-source
- build
- build-safari
- crowdin-push

View File

@@ -163,14 +163,14 @@ jobs:
matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
- name: Upload unix zip asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload unix checksum asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -324,14 +324,14 @@ jobs:
-t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload windows zip asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload windows checksum asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -339,7 +339,7 @@ jobs:
- name: Upload Chocolatey asset
if: matrix.license_type.build_prefix == 'bit'
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
@@ -350,7 +350,7 @@ jobs:
- name: Upload NPM Build Directory asset
if: matrix.license_type.build_prefix == 'bit'
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
@@ -421,14 +421,14 @@ jobs:
run: sudo snap remove bw
- name: Upload snap asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload snap checksum asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt

View File

@@ -207,7 +207,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -232,42 +232,42 @@ jobs:
run: npm run dist:lin
- name: Upload .deb artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
if-no-files-found: error
- name: Upload .rpm artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
if-no-files-found: error
- name: Upload .freebsd artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
if-no-files-found: error
- name: Upload .snap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload .AppImage artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release_channel }}-linux.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml
@@ -280,7 +280,7 @@ jobs:
sudo npm run pack:lin:flatpak
- name: Upload flatpak artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.bitwarden.desktop.flatpak
path: apps/desktop/dist/com.bitwarden.desktop.flatpak
@@ -373,7 +373,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -428,91 +428,91 @@ jobs:
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
- name: Upload portable exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload installer exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
if-no-files-found: error
- name: Upload store appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
if-no-files-found: error
- name: Upload NSIS ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
if-no-files-found: error
- name: Upload appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
if-no-files-found: error
- name: Upload store appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
if-no-files-found: error
- name: Upload NSIS x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
if-no-files-found: error
- name: Upload appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
if-no-files-found: error
- name: Upload store appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
if-no-files-found: error
- name: Upload nupkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release_channel }}.yml
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
@@ -561,14 +561,14 @@ jobs:
- name: Cache Build
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Cache Safari
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -681,7 +681,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -749,14 +749,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -869,7 +869,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -918,28 +918,28 @@ jobs:
run: npm run pack:mac
- name: Upload .zip artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
if-no-files-found: error
- name: Upload .dmg blockmap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release_channel }}-mac.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml
@@ -990,14 +990,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -1117,7 +1117,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -1166,7 +1166,7 @@ jobs:
run: npm run pack:mac:mas
- name: Upload .pkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
@@ -1193,7 +1193,7 @@ jobs:
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop')
uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0
uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0
with:
channel-id: C074F5UESQ0
payload: |
@@ -1252,14 +1252,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -1372,7 +1372,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -1424,7 +1424,7 @@ jobs:
zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app
- name: Upload masdev artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip

View File

@@ -164,7 +164,7 @@ jobs:
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
- name: Upload ${{ matrix.name }} artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
@@ -274,7 +274,7 @@ jobs:
- name: Build Docker image
id: build-docker
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
context: apps/web
file: apps/web/Dockerfile
@@ -303,14 +303,14 @@ jobs:
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
uses: anchore/scan-action@869c549e657a088dc0441b08ce4fc0ecdac2bb65 # v5.3.0
with:
image: ${{ steps.image-name.outputs.name }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}

View File

@@ -43,7 +43,7 @@ jobs:
- name: Cache NPM
id: npm-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }}
@@ -56,7 +56,7 @@ jobs:
run: npm run build-storybook:ci
- name: Publish to Chromatic
uses: chromaui/action@dd2eecb9bef44f54774581f4163b0327fd8cf607 # v11.16.3
uses: chromaui/action@64a9c0ca3bfb724389b0d536e544f56b7b5ff5b3 # v11.20.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@@ -22,7 +22,7 @@ jobs:
crowdin_project_id: "308189"
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@@ -158,42 +158,42 @@ jobs:
run: npm run dist:lin
- name: Upload .deb artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
if-no-files-found: error
- name: Upload .rpm artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
if-no-files-found: error
- name: Upload .freebsd artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
if-no-files-found: error
- name: Upload .snap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload .AppImage artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release-channel }}-linux.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml
@@ -299,91 +299,91 @@ jobs:
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
- name: Upload portable exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload installer exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
if-no-files-found: error
- name: Upload store appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
if-no-files-found: error
- name: Upload NSIS ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
if-no-files-found: error
- name: Upload appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
if-no-files-found: error
- name: Upload store appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
if-no-files-found: error
- name: Upload NSIS x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
if-no-files-found: error
- name: Upload appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
if-no-files-found: error
- name: Upload store appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
if-no-files-found: error
- name: Upload nupkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release-channel }}.yml
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml
@@ -426,14 +426,14 @@ jobs:
- name: Cache Build
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Cache Safari
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -560,14 +560,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -707,28 +707,28 @@ jobs:
run: npm run pack:mac
- name: Upload .zip artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
if-no-files-found: error
- name: Upload .dmg blockmap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release-channel }}-mac.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml
@@ -773,14 +773,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -915,7 +915,7 @@ jobs:
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Upload .pkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg

View File

@@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
@@ -115,7 +115,7 @@ jobs:
version: ${{ inputs.version_number_override }}
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
@@ -452,7 +452,7 @@ jobs:
- setup
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@@ -31,7 +31,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -46,7 +46,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: cx_result.sarif

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@@ -2105,7 +2105,7 @@
"message": "Aikakatkaisutoiminnon vahvistus"
},
"autoFillAndSave": {
"message": "Täytä automaattisesti ja tallenna"
"message": "Automaattitäytä ja tallenna"
},
"fillAndSave": {
"message": "Täytä ja tallenna"
@@ -3482,7 +3482,7 @@
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"message": "Aika jäljellä, ennen kuin nykyinen TOTP vanhenee",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
@@ -4810,7 +4810,7 @@
"message": "Määritä kaksivaiheinen kirjautuminen"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."

View File

@@ -1005,7 +1005,7 @@
"message": "Prikazuj identitete za jednostavnu auto-ispunu."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Klikni stavke za auto-ispunu na prikazu trezora"
},
"clearClipboard": {
"message": "Očisti međuspremnik",

View File

@@ -20,16 +20,16 @@
"message": "Crea account"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Nuovo in Bitwarden?"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Accedi con passkey"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Usa il Single Sign-On"
},
"welcomeBack": {
"message": "Welcome back"
"message": "Bentornato"
},
"setAStrongPassword": {
"message": "Imposta una password robusta"
@@ -120,7 +120,7 @@
"message": "Copia password"
},
"copyPassphrase": {
"message": "Copy passphrase"
"message": "Copia passphrase"
},
"copyNote": {
"message": "Copia nota"
@@ -153,13 +153,13 @@
"message": "Copia numero licenza"
},
"copyPrivateKey": {
"message": "Copy private key"
"message": "Copia chiave privata"
},
"copyPublicKey": {
"message": "Copy public key"
"message": "Copia chiave pubblica"
},
"copyFingerprint": {
"message": "Copy fingerprint"
"message": "Copia impronta"
},
"copyCustomField": {
"message": "Copia $FIELD$",
@@ -177,7 +177,7 @@
"message": "Copia note"
},
"fill": {
"message": "Fill",
"message": "Riempi",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@@ -193,10 +193,10 @@
"message": "Riempi automaticamente identità"
},
"fillVerificationCode": {
"message": "Fill verification code"
"message": "Riempi codice di verifica"
},
"fillVerificationCodeAria": {
"message": "Fill Verification Code",
"message": "Riempi Codice di Verifica",
"description": "Aria label for the heading displayed the inline menu for totp code autofill"
},
"generatePasswordCopied": {
@@ -239,7 +239,7 @@
"message": "Aggiungi elemento"
},
"accountEmail": {
"message": "Account email"
"message": "Email dell'account"
},
"requestHint": {
"message": "Richiedi suggerimento"
@@ -443,7 +443,7 @@
"message": "Genera password"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Genera passphrase"
},
"regeneratePassword": {
"message": "Rigenera password"
@@ -530,7 +530,7 @@
"description": "Label for the avoid ambiguous characters checkbox."
},
"generatorPolicyInEffect": {
"message": "Enterprise policy requirements have been applied to your generator options.",
"message": "I requisiti di politica aziendale sono stati applicati alle opzioni del generatore.",
"description": "Indicates that a policy limits the credential generator screen."
},
"searchVault": {
@@ -576,7 +576,7 @@
"message": "Note"
},
"privateNote": {
"message": "Private note"
"message": "Nota privata"
},
"note": {
"message": "Nota"
@@ -600,7 +600,7 @@
"message": "Avvia il sito web"
},
"launchWebsiteName": {
"message": "Launch website $ITEMNAME$",
"message": "Apri sito web $ITEMNAME$",
"placeholders": {
"itemname": {
"content": "$1",
@@ -633,7 +633,7 @@
"message": "Timeout della sessione"
},
"vaultTimeoutHeader": {
"message": "Vault timeout"
"message": "Timeout cassaforte"
},
"otherOptions": {
"message": "Altre opzioni"
@@ -651,13 +651,13 @@
"message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare."
},
"yourVaultIsLockedV2": {
"message": "Your vault is locked"
"message": "Cassaforte bloccata"
},
"yourAccountIsLocked": {
"message": "Your account is locked"
"message": "Il tuo account è bloccato"
},
"or": {
"message": "or"
"message": "o"
},
"unlock": {
"message": "Sblocca"
@@ -852,7 +852,7 @@
"message": "Accedi"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "Accedi a Bitwarden"
},
"restartRegistration": {
"message": "Riprova la registrazione"
@@ -888,10 +888,10 @@
"message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?"
},
"twoStepLoginConfirmationContent": {
"message": "Make your account more secure by setting up two-step login in the Bitwarden web app."
"message": "Rendi il tuo account più sicuro impostando l'autenticazione a due fattori nell'app web di Bitwarden."
},
"twoStepLoginConfirmationTitle": {
"message": "Continue to web app?"
"message": "Aprire web app?"
},
"editedFolder": {
"message": "Cartella salvata"
@@ -1005,7 +1005,7 @@
"message": "Mostra le identità nella sezione Scheda per riempirle automaticamente."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Clicca gli oggetti da riempire dalla sezione Cassaforte"
},
"clearClipboard": {
"message": "Cancella appunti",
@@ -1126,7 +1126,7 @@
"description": "WARNING (should stay in capitalized letters if the language permits)"
},
"warningCapitalized": {
"message": "Warning",
"message": "Attenzione",
"description": "Warning (should maintain locale-relevant capitalization)"
},
"confirmVaultExport": {
@@ -1206,7 +1206,7 @@
"message": "File"
},
"fileToShare": {
"message": "File to share"
"message": "File da condividere"
},
"selectFile": {
"message": "Seleziona un file"
@@ -1317,10 +1317,10 @@
"message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione."
},
"authenticationTimeout": {
"message": "Authentication timeout"
"message": "Timeout autenticazione"
},
"authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process."
"message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
},
"enterVerificationCodeEmail": {
"message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.",
@@ -1440,7 +1440,7 @@
"message": "URL del server"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "URL server autogestito",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@@ -1472,10 +1472,10 @@
"message": "Mostra suggerimenti di riempimento automatico nei campi del modulo"
},
"showInlineMenuIdentitiesLabel": {
"message": "Display identities as suggestions"
"message": "Mostra identità come consigli"
},
"showInlineMenuCardsLabel": {
"message": "Display cards as suggestions"
"message": "Mostra carte come consigli"
},
"showInlineMenuOnIconSelectionLabel": {
"message": "Mostra suggerimenti quando l'icona è selezionata"
@@ -1768,7 +1768,7 @@
"message": "Identità"
},
"typeSshKey": {
"message": "SSH key"
"message": "Chiave SSH"
},
"newItemHeader": {
"message": "Nuovo $TYPE$",
@@ -1801,13 +1801,13 @@
"message": "Cronologia delle password"
},
"generatorHistory": {
"message": "Generator history"
"message": "Cronologia generatore"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Cancella cronologia generatore"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
},
"back": {
"message": "Indietro"
@@ -1846,7 +1846,7 @@
"message": "Note sicure"
},
"sshKeys": {
"message": "SSH Keys"
"message": "Chiavi SSH"
},
"clear": {
"message": "Cancella",
@@ -1929,10 +1929,10 @@
"message": "Cancella cronologia"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Niente da mostrare"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "Non hai generato niente di recente"
},
"remove": {
"message": "Rimuovi"
@@ -1993,16 +1993,16 @@
"message": "Sblocca con PIN"
},
"setYourPinTitle": {
"message": "Set PIN"
"message": "Imposta PIN"
},
"setYourPinButton": {
"message": "Set PIN"
"message": "Imposta PIN"
},
"setYourPinCode": {
"message": "Imposta il tuo codice PIN per sbloccare Bitwarden. Le tue impostazioni PIN saranno resettate se esci completamente dall'app."
},
"setYourPinCode1": {
"message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden."
"message": "Il tuo PIN sarà usato per sbloccare Bitwarden invece della password principale. Il PIN sarà ripristinato se ti disconnetterai completamente da Bitwarden."
},
"pinRequired": {
"message": "Codice PIN obbligatorio."
@@ -2017,7 +2017,7 @@
"message": "Sblocca con i dati biometrici"
},
"unlockWithMasterPassword": {
"message": "Unlock with master password"
"message": "Sblocca con password principale"
},
"awaitDesktop": {
"message": "In attesa di conferma dal desktop"
@@ -2029,7 +2029,7 @@
"message": "Blocca con la password principale al riavvio del browser"
},
"lockWithMasterPassOnRestart1": {
"message": "Require master password on browser restart"
"message": "Richiedi password principale al riavvio del browser"
},
"selectOneCollection": {
"message": "Devi selezionare almeno una raccolta."
@@ -2067,7 +2067,7 @@
"message": "Azione timeout cassaforte"
},
"vaultTimeoutAction1": {
"message": "Timeout action"
"message": "Azione al timeout"
},
"lock": {
"message": "Blocca",
@@ -2355,14 +2355,14 @@
"message": "Modifiche del dominio escluso salvate"
},
"limitSendViews": {
"message": "Limit views"
"message": "Limita visualizzazioni"
},
"limitSendViewsHint": {
"message": "No one can view this Send after the limit is reached.",
"message": "Nessuno potrà vedere questo Send al raggiungimento del limite.",
"description": "Displayed under the limit views field on Send"
},
"limitSendViewsCount": {
"message": "$ACCESSCOUNT$ views left",
"message": "$ACCESSCOUNT$ visualizzazioni rimaste",
"description": "Displayed under the limit views field on Send",
"placeholders": {
"accessCount": {
@@ -2376,14 +2376,14 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDetails": {
"message": "Send details",
"message": "Dettagli Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeText": {
"message": "Testo"
},
"sendTypeTextToShare": {
"message": "Text to share"
"message": "Testo da condividere"
},
"sendTypeFile": {
"message": "File"
@@ -2393,7 +2393,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"hideTextByDefault": {
"message": "Hide text by default"
"message": "Nascondi testo come default"
},
"expired": {
"message": "Scaduto"
@@ -2440,7 +2440,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendPermanentConfirmation": {
"message": "Are you sure you want to permanently delete this Send?",
"message": "Sicuro di voler eliminare definitivamente questo Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
@@ -2451,7 +2451,7 @@
"message": "Data di eliminazione"
},
"deletionDateDescV2": {
"message": "The Send will be permanently deleted on this date.",
"message": "Il Send sarà cancellato definitivamente in questa data.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"expirationDate": {
@@ -2473,7 +2473,7 @@
"message": "Personalizzato"
},
"sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.",
"message": "Richiedi ai destinatari una password opzionale per aprire questo Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createSend": {
@@ -2500,11 +2500,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInHoursSingle": {
"message": "The Send will be available to anyone with the link for the next 1 hour.",
"message": "Il Send sarà disponibile a chiunque con il link per la prossima ora.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInHours": {
"message": "The Send will be available to anyone with the link for the next $HOURS$ hours.",
"message": "Il Send sarà disponibile a chiunque con il link per le prossime $HOURS$ ore.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": {
"hours": {
@@ -2514,11 +2514,11 @@
}
},
"sendExpiresInDaysSingle": {
"message": "The Send will be available to anyone with the link for the next 1 day.",
"message": "Il Send sarà disponibile a chiunque con il link per il prossimo giorno.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInDays": {
"message": "The Send will be available to anyone with the link for the next $DAYS$ days.",
"message": "Il Send sarà disponibile a chiunque con il link per i prossimi $DAYS$ giorni.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": {
"days": {
@@ -2536,11 +2536,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendFilePopoutDialogText": {
"message": "Pop out extension?",
"message": "Scollegare estensione?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendFilePopoutDialogDesc": {
"message": "To create a file Send, you need to pop out the extension to a new window.",
"message": "Per creare un file Send, devi scollegare l'estensione in una nuova finestra.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLinuxChromiumFileWarning": {
@@ -2553,7 +2553,7 @@
"message": "Per scegliere un file usando Safari, apri una nuova finestra cliccando questo banner."
},
"popOut": {
"message": "Pop out"
"message": "Scollega"
},
"sendFileCalloutHeader": {
"message": "Prima di iniziare"
@@ -2574,7 +2574,7 @@
"message": "Si è verificato un errore durante il salvataggio delle date di eliminazione e scadenza."
},
"hideYourEmail": {
"message": "Hide your email address from viewers."
"message": "Nascondi il tuo indirizzo email ai visualizzatori."
},
"passwordPrompt": {
"message": "Richiedi di inserire la password principale di nuovo per visualizzare questo elemento"
@@ -2631,7 +2631,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"message": "di $TOTAL$",
"placeholders": {
"total": {
"content": "$1",
@@ -2650,7 +2650,7 @@
"message": "Minuti"
},
"vaultTimeoutPolicyAffectingOptions": {
"message": "Enterprise policy requirements have been applied to your timeout options"
"message": "I requisiti di politica aziendale sono stati applicati alle opzioni di timeout"
},
"vaultTimeoutPolicyInEffect": {
"message": "Le politiche della tua organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.",
@@ -2666,7 +2666,7 @@
}
},
"vaultTimeoutPolicyInEffect1": {
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.",
"message": "Al massimo $HOURS$ ora/e e $MINUTES$ minuto/i.",
"placeholders": {
"hours": {
"content": "$1",
@@ -2679,7 +2679,7 @@
}
},
"vaultTimeoutPolicyMaximumError": {
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
"message": "Il timeout supera la restrizione impostata dalla tua organizzazione: massimo $HOURS$ ora/e e $MINUTES$ minuto/i",
"placeholders": {
"hours": {
"content": "$1",
@@ -2793,10 +2793,10 @@
"message": "Genera nome utente"
},
"generateEmail": {
"message": "Generate email"
"message": "Genera email"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@@ -2810,7 +2810,7 @@
}
},
"passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
"message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2820,7 +2820,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
"message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2861,11 +2861,11 @@
"message": "Genera un alias email con un servizio di inoltro esterno."
},
"forwarderDomainName": {
"message": "Email domain",
"message": "Dominio email",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "Scegli un dominio supportato dal servizio selezionato",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@@ -3068,25 +3068,25 @@
"message": "Invia notifica di nuovo"
},
"viewAllLogInOptions": {
"message": "View all log in options"
"message": "Visualizza tutte le opzioni di accesso"
},
"viewAllLoginOptionsV1": {
"message": "View all log in options"
"message": "Visualizza tutte le opzioni di accesso"
},
"notificationSentDevice": {
"message": "Una notifica è stata inviata al tuo dispositivo."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
"message": "Una notifica è stata inviata al tuo dispositivo"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
"message": "Sarai notificato una volta che la richiesta sarà approvata"
},
"needAnotherOptionV1": {
"message": "Need another option?"
"message": "Bisogno di un'altra opzione?"
},
"loginInitiated": {
"message": "Accesso avviato"
@@ -3182,16 +3182,16 @@
"message": "Si apre in una nuova finestra"
},
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless"
"message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
},
"deviceApprovalRequired": {
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
},
"deviceApprovalRequiredV2": {
"message": "Device approval required"
"message": "Approvazione dispositivo richiesta"
},
"selectAnApprovalOptionBelow": {
"message": "Select an approval option below"
"message": "Seleziona un'opzione di approvazione sotto"
},
"rememberThisDevice": {
"message": "Ricorda questo dispositivo"
@@ -3267,7 +3267,7 @@
"message": "Email utente mancante"
},
"activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out."
"message": "Email utente attiva non trovata. Logout in corso."
},
"deviceTrusted": {
"message": "Dispositivo fidato"
@@ -3478,11 +3478,11 @@
"description": "Screen reader text (aria-label) for unlock account button in overlay"
},
"totpCodeAria": {
"message": "Time-based One-Time Password Verification Code",
"message": "Codice di Verifica One-Time a tempo",
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"message": "Tempo rimasto prima che l'attuale TOTP scada",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
@@ -3711,10 +3711,10 @@
"message": "Passkey"
},
"accessing": {
"message": "Accessing"
"message": "Accedendo a"
},
"loggedInExclamation": {
"message": "Logged in!"
"message": "Accesso effettuato!"
},
"passkeyNotCopied": {
"message": "La passkey non sarà copiata"
@@ -3741,7 +3741,7 @@
"message": "Nessun login corrispondente per questo sito"
},
"searchSavePasskeyNewLogin": {
"message": "Search or save passkey as new login"
"message": "Cerca o salva la passkey come nuovo login"
},
"confirm": {
"message": "Conferma"
@@ -4208,13 +4208,13 @@
"message": "Filtri"
},
"filterVault": {
"message": "Filter vault"
"message": "Filtra cassaforte"
},
"filterApplied": {
"message": "One filter applied"
"message": "Un filtro applicato"
},
"filterAppliedPlural": {
"message": "$COUNT$ filters applied",
"message": "$COUNT$ filtri applicati",
"placeholders": {
"count": {
"content": "$1",
@@ -4328,7 +4328,7 @@
"message": "Abilita animazioni"
},
"showAnimations": {
"message": "Show animations"
"message": "Mostra animazioni"
},
"addAccount": {
"message": "Aggiungi account"
@@ -4546,13 +4546,13 @@
"message": "Posizione elemento"
},
"fileSend": {
"message": "File Send"
"message": "Send di File"
},
"fileSends": {
"message": "Send File"
},
"textSend": {
"message": "Text Send"
"message": "Send di Testo"
},
"textSends": {
"message": "Send Testo"
@@ -4570,7 +4570,7 @@
"message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione"
},
"showQuickCopyActions": {
"message": "Show quick copy actions on Vault"
"message": "Mostra azioni di copia rapida nella Cassaforte"
},
"systemDefault": {
"message": "Predefinito del sistema"
@@ -4579,37 +4579,37 @@
"message": "I requisiti della policy aziendale sono stati applicati a questa impostazione"
},
"sshPrivateKey": {
"message": "Private key"
"message": "Chiave privata"
},
"sshPublicKey": {
"message": "Public key"
"message": "Chiave pubblica"
},
"sshFingerprint": {
"message": "Fingerprint"
"message": "Impronta digitale"
},
"sshKeyAlgorithm": {
"message": "Key type"
"message": "Tipo di chiave"
},
"sshKeyAlgorithmED25519": {
"message": "ED25519"
},
"sshKeyAlgorithmRSA2048": {
"message": "RSA 2048-Bit"
"message": "RSA a 2048 bit"
},
"sshKeyAlgorithmRSA3072": {
"message": "RSA 3072-Bit"
"message": "RSA a 3072 bit"
},
"sshKeyAlgorithmRSA4096": {
"message": "RSA 4096-Bit"
"message": "RSA a 4096 bit"
},
"retry": {
"message": "Retry"
"message": "Riprova"
},
"vaultCustomTimeoutMinimum": {
"message": "Minimum custom timeout is 1 minute."
"message": "Il timeout personalizzato minimo è 1 minuto."
},
"additionalContentAvailable": {
"message": "Additional content is available"
"message": "Sono disponibili ulteriori contenuti"
},
"fileSavedToDevice": {
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
@@ -4642,22 +4642,22 @@
"message": "Non hai i permessi per modificare questo elemento"
},
"authenticating": {
"message": "Authenticating"
"message": "Autenticazione"
},
"fillGeneratedPassword": {
"message": "Fill generated password",
"message": "Riempi password generata",
"description": "Heading for the password generator within the inline menu"
},
"passwordRegenerated": {
"message": "Password regenerated",
"message": "Password rigenerata",
"description": "Notification message for when a password has been regenerated"
},
"saveLoginToBitwarden": {
"message": "Save login to Bitwarden?",
"message": "Salvare il login su Bitwarden?",
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
"message": "Space",
"message": "Spazio",
"description": "Represents the space key in screen reader content as a readable word"
},
"tildeCharacterDescriptor": {
@@ -4669,157 +4669,157 @@
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
"message": "Exclamation mark",
"message": "Punto esclamativo",
"description": "Represents the ! key in screen reader content as a readable word"
},
"atSignCharacterDescriptor": {
"message": "At sign",
"message": "Chiocciola",
"description": "Represents the @ key in screen reader content as a readable word"
},
"hashSignCharacterDescriptor": {
"message": "Hash sign",
"message": "Cancelletto",
"description": "Represents the # key in screen reader content as a readable word"
},
"dollarSignCharacterDescriptor": {
"message": "Dollar sign",
"message": "Simbolo del dollaro",
"description": "Represents the $ key in screen reader content as a readable word"
},
"percentSignCharacterDescriptor": {
"message": "Percent sign",
"message": "Segno di percentuale",
"description": "Represents the % key in screen reader content as a readable word"
},
"caretCharacterDescriptor": {
"message": "Caret",
"message": "Accento circonflesso",
"description": "Represents the ^ key in screen reader content as a readable word"
},
"ampersandCharacterDescriptor": {
"message": "Ampersand",
"message": "E commerciale",
"description": "Represents the & key in screen reader content as a readable word"
},
"asteriskCharacterDescriptor": {
"message": "Asterisk",
"message": "Asterisco",
"description": "Represents the * key in screen reader content as a readable word"
},
"parenLeftCharacterDescriptor": {
"message": "Left parenthesis",
"message": "Parentesi sinistra",
"description": "Represents the ( key in screen reader content as a readable word"
},
"parenRightCharacterDescriptor": {
"message": "Right parenthesis",
"message": "Parentesi destra",
"description": "Represents the ) key in screen reader content as a readable word"
},
"hyphenCharacterDescriptor": {
"message": "Underscore",
"message": "Trattino basso",
"description": "Represents the _ key in screen reader content as a readable word"
},
"underscoreCharacterDescriptor": {
"message": "Hyphen",
"message": "Trattino",
"description": "Represents the - key in screen reader content as a readable word"
},
"plusCharacterDescriptor": {
"message": "Plus",
"message": "P",
"description": "Represents the + key in screen reader content as a readable word"
},
"equalsCharacterDescriptor": {
"message": "Equals",
"message": "Uguale",
"description": "Represents the = key in screen reader content as a readable word"
},
"braceLeftCharacterDescriptor": {
"message": "Left brace",
"message": "Parentesi graffa aperta",
"description": "Represents the { key in screen reader content as a readable word"
},
"braceRightCharacterDescriptor": {
"message": "Right brace",
"message": "Parentesi graffa chiusa",
"description": "Represents the } key in screen reader content as a readable word"
},
"bracketLeftCharacterDescriptor": {
"message": "Left bracket",
"message": "Parentesi quadra aperta",
"description": "Represents the [ key in screen reader content as a readable word"
},
"bracketRightCharacterDescriptor": {
"message": "Right bracket",
"message": "Parentesi quadra chiusa",
"description": "Represents the ] key in screen reader content as a readable word"
},
"pipeCharacterDescriptor": {
"message": "Pipe",
"message": "Barra verticale",
"description": "Represents the | key in screen reader content as a readable word"
},
"backSlashCharacterDescriptor": {
"message": "Back slash",
"message": "Barra rovesciata",
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
"message": "Colon",
"message": "Due punti",
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "Punto e virgola",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {
"message": "Double quote",
"message": "Doppi apici",
"description": "Represents the double quote key in screen reader content as a readable word"
},
"singleQuoteCharacterDescriptor": {
"message": "Single quote",
"message": "Apostrofo",
"description": "Represents the ' key in screen reader content as a readable word"
},
"lessThanCharacterDescriptor": {
"message": "Less than",
"message": "Minore",
"description": "Represents the < key in screen reader content as a readable word"
},
"greaterThanCharacterDescriptor": {
"message": "Greater than",
"message": "Maggiore",
"description": "Represents the > key in screen reader content as a readable word"
},
"commaCharacterDescriptor": {
"message": "Comma",
"message": "Virgola",
"description": "Represents the , key in screen reader content as a readable word"
},
"periodCharacterDescriptor": {
"message": "Period",
"message": "Punto",
"description": "Represents the . key in screen reader content as a readable word"
},
"questionCharacterDescriptor": {
"message": "Question mark",
"message": "Punto interrogativo",
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {
"message": "Forward slash",
"message": "Slash",
"description": "Represents the / key in screen reader content as a readable word"
},
"lowercaseAriaLabel": {
"message": "Lowercase"
"message": "Minuscolo"
},
"uppercaseAriaLabel": {
"message": "Uppercase"
"message": "Maiuscolo"
},
"generatedPassword": {
"message": "Generated password"
"message": "Password generata"
},
"compactMode": {
"message": "Compact mode"
"message": "Modalità compatta"
},
"beta": {
"message": "Beta"
},
"importantNotice": {
"message": "Important notice"
"message": "Avviso importante"
},
"setupTwoStepLogin": {
"message": "Set up two-step login"
"message": "Imposta accesso in due passaggi"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden invierà un codice all'email del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
},
"remindMeLater": {
"message": "Remind me later"
"message": "Ricordamelo più tardi"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
"message": "Riesci ancora ad accedere a questa email, $EMAIL$?",
"placeholders": {
"email": {
"content": "$1",
@@ -4828,24 +4828,24 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not"
"message": "No, non riesco"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "Sì, riesco ad accedere a questa email"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "Attiva accesso in due passaggi"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "Cambia l'email dell'account"
},
"extensionWidth": {
"message": "Extension width"
"message": "Larghezza estensione"
},
"wide": {
"message": "Wide"
"message": "Larga"
},
"extraWide": {
"message": "Extra wide"
"message": "Molto larga"
}
}

View File

@@ -1005,7 +1005,7 @@
"message": "自動入力を簡単にするために、タブページに ID アイテムを表示します"
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "保管庫で、自動入力するアイテムをクリックしてください"
},
"clearClipboard": {
"message": "クリップボードの消去",
@@ -4810,10 +4810,10 @@
"message": "2段階認証を設定する"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
},
"remindMeLater": {
"message": "後で再通知"
@@ -4831,13 +4831,13 @@
"message": "いいえ、違います。"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "はい、メールアドレスには私が確実にアクセスできます"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "2段階認証によるログインを有効にする"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "アカウントのメールアドレスを変更する"
},
"extensionWidth": {
"message": "拡張機能の幅"

View File

@@ -181,7 +181,7 @@
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
"message": "Auto-invullen"
"message": "Automatisch invullen"
},
"autoFillLogin": {
"message": "Login automatisch invullen"
@@ -688,7 +688,7 @@
"message": "Nu vergrendelen"
},
"lockAll": {
"message": "Vergrendel alles"
"message": "Alles vergrendelen"
},
"immediately": {
"message": "Onmiddellijk"
@@ -1043,7 +1043,7 @@
"message": "Ja, nu bijwerken"
},
"notificationUnlockDesc": {
"message": "Ontgrendel je Bitwarden-kluis om het auto-invulverzoek te voltooien."
"message": "Ontgrendel je Bitwarden-kluis om het automatisch invullen verzoek te voltooien."
},
"notificationUnlock": {
"message": "Ontgrendelen"
@@ -1200,7 +1200,7 @@
"message": "Geen bijlagen."
},
"attachmentSaved": {
"message": "De bijlage is opgeslagen."
"message": "Bijlage opgeslagen"
},
"file": {
"message": "Bestand"
@@ -1209,7 +1209,7 @@
"message": "Bestand om te delen"
},
"selectFile": {
"message": "Selecteer een bestand."
"message": "Selecteer een bestand"
},
"maxFileSize": {
"message": "Maximale bestandsgrootte is 500 MB."
@@ -1242,7 +1242,7 @@
"message": "1 GB versleutelde opslag voor bijlagen."
},
"premiumSignUpEmergency": {
"message": "Noodtoegang"
"message": "Noodtoegang."
},
"premiumSignUpTwoStepOptions": {
"message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo."
@@ -1425,10 +1425,10 @@
"message": "Specificeer de basis-URL van je zelfgehoste Bitwarden-installatie. Bijvoorbeeld: https://bitwarden.company.com"
},
"selfHostedCustomEnvHeader": {
"message": "For advanced configuration, you can specify the base URL of each service independently."
"message": "Voor geavanceerde configuratie kun je de basis URL van elke service onafhankelijk opgeven."
},
"selfHostedEnvFormInvalid": {
"message": "You must add either the base Server URL or at least one custom environment."
"message": "Je moet de basis URL van de server of ten minste één aangepaste omgeving toevoegen."
},
"customEnvironment": {
"message": "Aangepaste omgeving"
@@ -1459,14 +1459,14 @@
"message": "Pictogrammenserver-URL"
},
"environmentSaved": {
"message": "De omgeving-URL's zijn opgeslagen."
"message": "Omgeving-URL's opgeslagen"
},
"showAutoFillMenuOnFormFields": {
"message": "Auto-invulmenu op formuliervelden weergeven",
"message": "Automatisch invullen menu op formuliervelden weergeven",
"description": "Represents the message for allowing the user to enable the autofill overlay"
},
"autofillSuggestionsSectionTitle": {
"message": "Suggesties voor automatisch invullen"
"message": "Suggesties automatisch invullen"
},
"showInlineMenuLabel": {
"message": "Suggesties voor automatisch invullen op formuliervelden weergeven"
@@ -1511,7 +1511,7 @@
"message": "Als een inlogformulier wordt gedetecteerd, dan worden de inloggegevens automatisch ingevuld."
},
"experimentalFeature": {
"message": "Gehackte of onbetrouwbare websites kunnen auto-invullen tijdens het laden van de pagina misbruiken."
"message": "Gehackte of onbetrouwbare websites kunnen automatisch invullen tijdens het laden van de pagina misbruiken."
},
"learnMoreAboutAutofillOnPageLoadLinkText": {
"message": "Meer over risico's lezen"
@@ -1535,7 +1535,7 @@
"message": "Automatisch invullen bij laden van pagina"
},
"autoFillOnPageLoadNo": {
"message": "Niet Automatisch invullen bij laden van pagina"
"message": "Niet automatisch invullen bij laden van pagina"
},
"commandOpenPopup": {
"message": "Open kluis in pop-up"
@@ -1606,7 +1606,7 @@
"message": "Een herkenbare afbeelding naast iedere login weergeven."
},
"faviconDescAlt": {
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
"message": "Toon een herkenbare afbeelding naast elke login. Geldt voor alle ingelogde accounts."
},
"enableBadgeCounter": {
"message": "Teller weergeven"
@@ -1690,7 +1690,7 @@
"message": "Dr."
},
"mx": {
"message": "Mx"
"message": "Mx."
},
"firstName": {
"message": "Voornaam"
@@ -1993,10 +1993,10 @@
"message": "Ontgrendelen met PIN"
},
"setYourPinTitle": {
"message": "PIN-code instellen"
"message": "PIN instellen"
},
"setYourPinButton": {
"message": "PIN-code instellen"
"message": "PIN instellen"
},
"setYourPinCode": {
"message": "Stel je PIN-code in voor het ontgrendelen van Bitwarden. Je PIN-code wordt opnieuw ingesteld als je je ooit volledig afmeldt bij de applicatie."
@@ -2111,10 +2111,10 @@
"message": "Invullen en opslaan"
},
"autoFillSuccessAndSavedUri": {
"message": "Automatisch gevuld item en opgeslagen URI"
"message": "Automatisch ingevuld item en opgeslagen URI"
},
"autoFillSuccess": {
"message": "Automatisch gevuld item"
"message": "Item automatisch ingevuld "
},
"insecurePageWarning": {
"message": "Waarschuwing: Dit is een onbeveiligde HTTP-pagina waardoor anderen alle informatie die je verstuurt kunnen zien en wijzigen. Deze login is oorspronkelijk opgeslagen op een beveiligde (HTTPS) pagina."
@@ -2225,7 +2225,7 @@
"message": "Fout bij vernieuwen toegangstoken"
},
"errorRefreshingAccessTokenDesc": {
"message": "No refresh token or API keys found. Please try logging out and logging back in."
"message": "Geen verversingstoken of API-sleutels gevonden. Probeer uit te loggen en weer in te loggen."
},
"desktopSyncVerificationTitle": {
"message": "Desktopsynchronisatieverificatie"
@@ -2282,10 +2282,10 @@
"message": "Dit apparaat ondersteunt geen browserbiometrie."
},
"biometricsNotUnlockedTitle": {
"message": "User locked or logged out"
"message": "Gebruiker vergrendeld of uitgelogd"
},
"biometricsNotUnlockedDesc": {
"message": "Please unlock this user in the desktop application and try again."
"message": "Ontgrendel deze gebruiker in de desktopapplicatie en probeer het opnieuw."
},
"biometricsNotAvailableTitle": {
"message": "Biometrisch ontgrendelen niet beschikbaar"
@@ -2623,11 +2623,11 @@
"description": "Used as a message within the notification bar when no folders are found"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",
"message": "De machtigingen van je organisatie zijn bijgewerkt, waardoor je een hoofdwachtwoord moet instellen.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.",
"message": "Je organisatie vereist dat je een hoofdwachtwoord instelt.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
@@ -2766,7 +2766,7 @@
"message": "Persoonlijke kluis exporteren"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"message": "Alleen de individuele kluisitems die gekoppeld zijn aan $EMAIL$ worden geëxporteerd. Kluisitems van organisaties worden niet meegenomen. Alleen de informatie over het kluisitem wordt geëxporteerd en niet de bijbehorende bijlagen.",
"placeholders": {
"email": {
"content": "$1",
@@ -2869,7 +2869,7 @@
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
"message": "$SERVICENAME$ error: $ERRORMESSAGE$",
"message": "$SERVICENAME$ fout: $ERRORMESSAGE$",
"description": "Reports an error returned by a forwarding service to the user.",
"placeholders": {
"servicename": {
@@ -2887,7 +2887,7 @@
"description": "Displayed with the address on the forwarding service's configuration screen."
},
"forwarderGeneratedByWithWebsite": {
"message": "Website: $WEBSITE$. Generated by Bitwarden.",
"message": "Website: $WEBSITE$. Gegenereerd door Bitwarden.",
"description": "Displayed with the address on the forwarding service's configuration screen.",
"placeholders": {
"WEBSITE": {
@@ -2897,7 +2897,7 @@
}
},
"forwaderInvalidToken": {
"message": "Invalid $SERVICENAME$ API token",
"message": "Ongeldig $SERVICENAME$ API token",
"description": "Displayed when the user's API token is empty or rejected by the forwarding service.",
"placeholders": {
"servicename": {
@@ -2907,7 +2907,7 @@
}
},
"forwaderInvalidTokenWithMessage": {
"message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$",
"message": "Ongeldige $SERVICENAME$ API token: $ERRORMESSAGE$",
"description": "Displayed when the user's API token is rejected by the forwarding service with an error message.",
"placeholders": {
"servicename": {
@@ -2921,7 +2921,7 @@
}
},
"forwarderNoAccountId": {
"message": "Unable to obtain $SERVICENAME$ masked email account ID.",
"message": "Kan $SERVICENAME$ gemaskeerde e-mailaccount-ID niet verkrijgen.",
"description": "Displayed when the forwarding service fails to return an account ID.",
"placeholders": {
"servicename": {
@@ -2931,7 +2931,7 @@
}
},
"forwarderNoDomain": {
"message": "Invalid $SERVICENAME$ domain.",
"message": "Ongeldig $SERVICENAME$ domein.",
"description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.",
"placeholders": {
"servicename": {
@@ -2941,7 +2941,7 @@
}
},
"forwarderNoUrl": {
"message": "Invalid $SERVICENAME$ url.",
"message": "Ongeldige $SERVICENAME$ url.",
"description": "Displayed when the url of the forwarding service wasn't supplied.",
"placeholders": {
"servicename": {
@@ -2951,7 +2951,7 @@
}
},
"forwarderUnknownError": {
"message": "Unknown $SERVICENAME$ error occurred.",
"message": "Onbekende $SERVICENAME$ fout opgetreden.",
"description": "Displayed when the forwarding service failed due to an unknown error.",
"placeholders": {
"servicename": {
@@ -2961,7 +2961,7 @@
}
},
"forwarderUnknownForwarder": {
"message": "Unknown forwarder: '$SERVICENAME$'.",
"message": "Onbekende doorstuurder: '$SERVICENAME$'.",
"description": "Displayed when the forwarding service is not supported.",
"placeholders": {
"servicename": {
@@ -3017,7 +3017,7 @@
"message": "zelfgehost"
},
"thirdParty": {
"message": "van derden"
"message": "Derde partij"
},
"thirdPartyServerMessage": {
"message": "Verbonden met server implementatie van derden, $SERVERNAME$. Reproduceer bugs via de officiële server, of meld ze bij het serverproject.",
@@ -3071,7 +3071,7 @@
"message": "Alle inlogopties bekijken"
},
"viewAllLoginOptionsV1": {
"message": "View all log in options"
"message": "Alle inlogopties weergeven"
},
"notificationSentDevice": {
"message": "Er is een melding naar je apparaat verzonden."
@@ -3125,10 +3125,10 @@
"message": "Je organisatiebeleid heeft het automatisch invullen bij laden van pagina ingeschakeld."
},
"howToAutofill": {
"message": "Hoe automatisch aanvullen"
"message": "Hoe automatisch invullen"
},
"autofillSelectInfoWithCommand": {
"message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.",
"message": "Selecteer een item in dit scherm, gebruik de sneltoets $COMMAND$ of verken andere opties in de instellingen.",
"placeholders": {
"command": {
"content": "$1",
@@ -3137,16 +3137,16 @@
}
},
"autofillSelectInfoWithoutCommand": {
"message": "Select an item from this screen, or explore other options in settings."
"message": "Selecteer een item in dit scherm of verken andere opties in de instellingen."
},
"gotIt": {
"message": "Ik snap het"
"message": "Oké"
},
"autofillSettings": {
"message": "Instellingen automatisch invullen"
},
"autofillKeyboardShortcutSectionTitle": {
"message": "Shortcut voor automatisch invullen"
"message": "Snelkoppeling automatisch invullen"
},
"autofillKeyboardShortcutUpdateLabel": {
"message": "Snelkoppeling wijzigen"
@@ -3243,7 +3243,7 @@
"message": "Algemeen"
},
"display": {
"message": "Display"
"message": "Weergave"
},
"accountSuccessfullyCreated": {
"message": "Account succesvol aangemaakt!"
@@ -3372,7 +3372,7 @@
"message": "-- Type om te filteren --"
},
"multiSelectLoading": {
"message": "Opties ophalen..."
"message": "Opties ophalen"
},
"multiSelectNotFound": {
"message": "Geen items gevonden"
@@ -3413,7 +3413,7 @@
"description": "Notification button text for starting a fileless import."
},
"importing": {
"message": "Importeren...",
"message": "Importeren",
"description": "Notification message for when an import is in progress."
},
"dataSuccessfullyImported": {
@@ -3432,33 +3432,33 @@
"message": "Aliasdomein"
},
"passwordRepromptDisabledAutofillOnPageLoad": {
"message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.",
"message": "Items met hoofdwachtwoord re-prompt kunnen niet automatisch worden ingevuld bij het laden van de pagina. Automatisch invullen bij laden van pagina uitgeschakeld.",
"description": "Toast message for describing that master password re-prompt cannot be autofilled on page load."
},
"autofillOnPageLoadSetToDefault": {
"message": "Autofill on page load set to use default setting.",
"message": "Automatisch invullen bij het laden van een pagina ingesteld op de standaardinstelling.",
"description": "Toast message for informing the user that autofill on page load has been set to the default setting."
},
"turnOffMasterPasswordPromptToEditField": {
"message": "Turn off master password re-prompt to edit this field",
"message": "Hoofdwachtwoord herhaalprompt uitschakelen om dit veld te bewerken",
"description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item."
},
"toggleSideNavigation": {
"message": "Zijnavigatie schakelen"
},
"skipToContent": {
"message": "Skip to content"
"message": "Overslaan naar inhoud"
},
"bitwardenOverlayButton": {
"message": "Menuknop Bitwarden automatisch invullen",
"description": "Page title for the iframe containing the overlay button"
},
"toggleBitwardenVaultOverlay": {
"message": "Bitwarden auto-invulmenu in- en uitschakelen",
"message": "Bitwarden automatisch invullen menu in- en uitschakelen",
"description": "Screen reader and tool tip label for the overlay button"
},
"bitwardenVault": {
"message": "Bitwarden auto-invulmenu",
"message": "Bitwarden automatisch invullen menu",
"description": "Page title in overlay"
},
"unlockYourAccountToViewMatchingLogins": {
@@ -3486,7 +3486,7 @@
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
"message": "Inloggegevens invullen voor",
"message": "Referenties invullen voor",
"description": "Screen reader text for when overlay item is in focused"
},
"partialUsername": {
@@ -3530,7 +3530,7 @@
"description": "Screen reader text (aria-label) for new identity button within inline menu"
},
"bitwardenOverlayMenuAvailable": {
"message": "Bitwarden auto-invulmenu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.",
"message": "Bitwarden automatisch invullen menu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.",
"description": "Screen reader text for announcing when the overlay opens on the page"
},
"turnOn": {
@@ -3574,7 +3574,7 @@
"message": "Deze actie vereist verificatie actie. Stel een pincode in om door te gaan."
},
"setPin": {
"message": "Pincode instellen"
"message": "PIN instellen"
},
"verifyWithBiometrics": {
"message": "Biometrisch ontgrendelen"
@@ -3619,7 +3619,7 @@
"message": "Fout bij het verbinden met de Duo-service. Gebruik een andere tweestapsaanmeldingsmethode of neem contact op met Duo voor hulp."
},
"launchDuoAndFollowStepsToFinishLoggingIn": {
"message": "Launch Duo and follow the steps to finish logging in."
"message": "Start Duo en volg de stappen om het inloggen te voltooien."
},
"duoRequiredForAccount": {
"message": "Jouw account vereist Duo-tweestapsaanmelding."
@@ -3628,10 +3628,10 @@
"message": "Open de extensie om in te loggen."
},
"popoutExtension": {
"message": "Popout extension"
"message": "Pop-out extensie"
},
"launchDuo": {
"message": "Launch Duo"
"message": "Duo starten"
},
"importFormatError": {
"message": "De gegevens zijn niet correct opgemaakt. Controleer je importbestand en probeer het opnieuw."
@@ -3759,7 +3759,7 @@
"message": "Kies een passkey om mee in te loggen"
},
"passkeyItem": {
"message": "Passkey-Item"
"message": "Passkey item"
},
"overwritePasskey": {
"message": "Passkey overschrijven?"
@@ -3801,7 +3801,7 @@
"message": "LastPass Email"
},
"importingYourAccount": {
"message": "Account impoteren..."
"message": "Account impoteren"
},
"lastPassMFARequired": {
"message": "LastPass multifactor-authenticatie vereist"
@@ -3825,10 +3825,10 @@
"message": "Wacht op SSO-authenticatie"
},
"awaitingSSODesc": {
"message": "Ga door met inloggen met de inloggegevens van je bedrijf."
"message": "Ga door met inloggen met de referenties van je bedrijf."
},
"seeDetailedInstructions": {
"message": "See detailed instructions on our help site at",
"message": "Zie gedetailleerde instructies op onze helpsite op",
"description": "This is followed a by a hyperlink to the help website."
},
"importDirectlyFromLastPass": {
@@ -3844,52 +3844,52 @@
"message": "Collectie"
},
"lastPassYubikeyDesc": {
"message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button."
"message": "Steek de YubiKey die bij je LastPass account hoort in de USB poort van je computer en druk dan op de knop."
},
"switchAccount": {
"message": "Switch account"
"message": "Account wisselen"
},
"switchAccounts": {
"message": "Switch accounts"
"message": "Accounts wisselen"
},
"switchToAccount": {
"message": "Switch to account"
"message": "Wisselen naar account"
},
"activeAccount": {
"message": "Active account"
"message": "Actief account"
},
"availableAccounts": {
"message": "Beschikbare accounts"
},
"accountLimitReached": {
"message": "Account limit reached. Log out of an account to add another."
"message": "Accountlimiet bereikt. Log uit bij een account om een andere toe te voegen."
},
"active": {
"message": "active"
"message": "actief"
},
"locked": {
"message": "locked"
"message": "vergrendeld"
},
"unlocked": {
"message": "unlocked"
"message": "ontgrendeld"
},
"server": {
"message": "server"
},
"hostedAt": {
"message": "hosted at"
"message": "gehost op"
},
"useDeviceOrHardwareKey": {
"message": "Use your device or hardware key"
"message": "Gebruik je apparaat of hardwaresleutel"
},
"justOnce": {
"message": "Just once"
"message": "Eenmalig"
},
"alwaysForThisSite": {
"message": "Always for this site"
"message": "Altijd voor deze site"
},
"domainAddedToExcludedDomains": {
"message": "$DOMAIN$ added to excluded domains.",
"message": "$DOMAIN$ toegevoegd aan uitgesloten domeinen.",
"placeholders": {
"domain": {
"content": "$1",
@@ -3950,7 +3950,7 @@
"description": "Button text for the setting that allows overriding the default browser autofill settings"
},
"saveCipherAttemptSuccess": {
"message": "Credentials saved successfully!",
"message": "Referenties succesvol opgeslagen!",
"description": "Notification message for when saving credentials has succeeded."
},
"passwordSaved": {
@@ -3958,7 +3958,7 @@
"description": "Notification message for when saving credentials has succeeded."
},
"updateCipherAttemptSuccess": {
"message": "Credentials updated successfully!",
"message": "Referenties succesvol bijgewerkt!",
"description": "Notification message for when updating credentials has succeeded."
},
"passwordUpdated": {
@@ -3966,7 +3966,7 @@
"description": "Notification message for when updating credentials has succeeded."
},
"saveCipherAttemptFailed": {
"message": "Error saving credentials. Check console for details.",
"message": "Fout bij het opslaan van referenties. Controleer console voor details.",
"description": "Notification message for when saving credentials has failed."
},
"success": {
@@ -3979,7 +3979,7 @@
"message": "Passkey verwijderd"
},
"autofillSuggestions": {
"message": "Suggesties voor automatisch invullen"
"message": "Suggesties automatisch invullen"
},
"autofillSuggestionsTip": {
"message": "Inlogitem opslaan voor automatisch invullen op deze site"
@@ -3994,7 +3994,7 @@
"message": "Wis filters of probeer een andere zoekterm"
},
"copyInfoTitle": {
"message": "Copy info - $ITEMNAME$",
"message": "Info kopiëren - $ITEMNAME$",
"description": "Title for a button that opens a menu with options to copy information from an item.",
"placeholders": {
"itemname": {
@@ -4004,7 +4004,7 @@
}
},
"copyNoteTitle": {
"message": "Copy Note - $ITEMNAME$",
"message": "Notitie kopiëren - $ITEMNAME$",
"description": "Title for a button copies a note to the clipboard.",
"placeholders": {
"itemname": {
@@ -4014,7 +4014,7 @@
}
},
"moreOptionsLabel": {
"message": "More options, $ITEMNAME$",
"message": "Meer opties, $ITEMNAME$",
"description": "Aria label for a button that opens a menu with more options for an item.",
"placeholders": {
"itemname": {
@@ -4024,7 +4024,7 @@
}
},
"moreOptionsTitle": {
"message": "More options - $ITEMNAME$",
"message": "Meer opties - $ITEMNAME$",
"description": "Title for a button that opens a menu with more options for an item.",
"placeholders": {
"itemname": {
@@ -4034,7 +4034,7 @@
}
},
"viewItemTitle": {
"message": "View item - $ITEMNAME$",
"message": "Item bekijken - $ITEMNAME$",
"description": "Title for a link that opens a view for an item.",
"placeholders": {
"itemname": {
@@ -4060,22 +4060,22 @@
"message": "Aan collecties toewijzen"
},
"copyEmail": {
"message": "Copy email"
"message": "E-mail kopiëren"
},
"copyPhone": {
"message": "Copy phone"
"message": "Telefoon kopiëren"
},
"copyAddress": {
"message": "Copy address"
"message": "Adres kopiëren"
},
"adminConsole": {
"message": "Admin Console"
"message": "Beheerconsole"
},
"accountSecurity": {
"message": "Accountbeveiliging"
},
"notifications": {
"message": "Notifications"
"message": "Meldingen"
},
"appearance": {
"message": "Uiterlijk"
@@ -4107,7 +4107,7 @@
}
},
"new": {
"message": "New"
"message": "Nieuw"
},
"removeItem": {
"message": "Verwijder $NAME$",
@@ -4245,7 +4245,7 @@
"description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher."
},
"loginCredentials": {
"message": "Inloggegevens"
"message": "Login referenties"
},
"authenticatorKey": {
"message": "Authenticatiesleutel"
@@ -4546,10 +4546,10 @@
"message": "Itemlocatie"
},
"fileSend": {
"message": "Bestand-Sends"
"message": "Bestand verzenden"
},
"fileSends": {
"message": "Bestand-Sends"
"message": "Bestanden verzenden"
},
"textSend": {
"message": "Tekst-Sends"
@@ -4567,7 +4567,7 @@
"message": "Accountacties"
},
"showNumberOfAutofillSuggestions": {
"message": "Aantal login-autofill-suggesties op het extensie-pictogram weergeven"
"message": "Aantal login automatisch invullen suggesties op het extensie-pictogram weergeven"
},
"showQuickCopyActions": {
"message": "Toon snelle kopieeracties in de kluis"

View File

@@ -1005,7 +1005,7 @@
"message": "Pokaż elementy tożsamości na stronie głównej, aby ułatwić autouzupełnianie."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Kliknij na dane logowania, aby autouzupełnić w widoku Sejfu"
},
"clearClipboard": {
"message": "Wyczyść schowek",

View File

@@ -803,7 +803,7 @@
"message": "Código de verificação inválido"
},
"valueCopied": {
"message": "$VALUE$ copiado",
"message": "$VALUE$ copiado(a)",
"description": "Value has been copied to the clipboard.",
"placeholders": {
"value": {

View File

@@ -193,10 +193,10 @@
"message": "Ауто-пуњење идентитета"
},
"fillVerificationCode": {
"message": "Fill verification code"
"message": "Пуни верификациони кôд"
},
"fillVerificationCodeAria": {
"message": "Fill Verification Code",
"message": "Пуни верификациони кôд",
"description": "Aria label for the heading displayed the inline menu for totp code autofill"
},
"generatePasswordCopied": {
@@ -1005,7 +1005,7 @@
"message": "Прикажи ставке идентитета на страници за лакше аутоматско допуњавање."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Кликните на ставке за ауто-попуњавање у приказу сефа"
},
"clearClipboard": {
"message": "Обриши привремену меморију",
@@ -3071,7 +3071,7 @@
"message": "Погледајте сав извештај у опције"
},
"viewAllLoginOptionsV1": {
"message": "View all log in options"
"message": "Погледајте сав извештај у опције"
},
"notificationSentDevice": {
"message": "Обавештење је послато на ваш уређај."
@@ -3478,11 +3478,11 @@
"description": "Screen reader text (aria-label) for unlock account button in overlay"
},
"totpCodeAria": {
"message": "Time-based One-Time Password Verification Code",
"message": "Једнократни верификациони кôд заснован на времену",
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"message": "Преостало време до истека актуелног ТОТП-а",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
@@ -4570,7 +4570,7 @@
"message": "Прикажи број предлога за ауто-попуњавање пријаве на икони додатка"
},
"showQuickCopyActions": {
"message": "Show quick copy actions on Vault"
"message": "Приказати брзе радње копирања у Сефу"
},
"systemDefault": {
"message": "Системски подразумевано"
@@ -4804,22 +4804,22 @@
"message": "Бета"
},
"importantNotice": {
"message": "Important notice"
"message": "Важно обавештење"
},
"setupTwoStepLogin": {
"message": "Set up two-step login"
"message": "Поставити дво-степенску пријаву"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden ће послати кôд на имејл вашег налога за верификовање пријављивања са нових уређаја почевши од фебруара 2025."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "Можете да подесите пријаву у два корака као алтернативни начин да заштитите свој налог или да промените свој имејл у један који можете да приступите."
},
"remindMeLater": {
"message": "Remind me later"
"message": "Подсети ме касније"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
"message": "Да ли имате поуздан приступ својим имејлом, $EMAIL$?",
"placeholders": {
"email": {
"content": "$1",
@@ -4828,16 +4828,16 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not"
"message": "Не, ненам"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "Да, могу поуздано да приступим овим имејлом"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "Упалити дво-степенску пријаву"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "Променити имејл налога"
},
"extensionWidth": {
"message": "Ширина додатка"

View File

@@ -4154,7 +4154,7 @@
"message": "Ek bilgiler"
},
"itemHistory": {
"message": "Öğe geçmişi"
"message": "Kayıt geçmişi"
},
"lastEdited": {
"message": "Son düzenlenme"

View File

@@ -1005,7 +1005,7 @@
"message": "Показувати список посвідчень на сторінці вкладки для легкого автозаповнення."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Натисніть на запис у режимі перегляду сховища для автозаповнення"
},
"clearClipboard": {
"message": "Очистити буфер обміну",

View File

@@ -284,7 +284,7 @@
"message": "前往网页 App 吗?"
},
"continueToWebAppDesc": {
"message": "在网页应用上探索 Bitwarden 账户的更多功能。"
"message": "在网页 App 上探索 Bitwarden 账户的更多功能。"
},
"continueToHelpCenter": {
"message": "前往帮助中心吗?"
@@ -299,7 +299,7 @@
"message": "帮助别人了解 Bitwarden 是否适合他们。立即访问浏览器的扩展程序商店并留下评分。"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。"
"message": "您可以在 Bitwarden 网页 App 上更改您的主密码。"
},
"fingerprintPhrase": {
"message": "指纹短语",
@@ -428,7 +428,7 @@
"description": "Short for 'credential generator'."
},
"passGenInfo": {
"message": "自动生成安全可靠唯一的登录密码。"
"message": "自动为您的登录生成强大且唯一的密码。"
},
"bitWebVaultApp": {
"message": "Bitwarden 网页 App"
@@ -597,7 +597,7 @@
"message": "前往"
},
"launchWebsite": {
"message": "启动网站"
"message": "打开网站"
},
"launchWebsiteName": {
"message": "前往 $ITEMNAME$ 的网站",
@@ -763,7 +763,7 @@
"message": "必须填写确认主密码。"
},
"masterPasswordMinlength": {
"message": "主密码必须至少 $VALUE$ 个字符长度。",
"message": "主密码长度必须至少 $VALUE$ 个字符。",
"description": "The Master Password must be at least a specific number of characters long.",
"placeholders": {
"value": {
@@ -888,7 +888,7 @@
"message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件来验证您的登录这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?"
},
"twoStepLoginConfirmationContent": {
"message": "通过在 Bitwarden 网页 App 中设置两步登录,可以使您的账户更加安全。"
"message": "在 Bitwarden 网页 App 中设置两步登录,您的账户更加安全。"
},
"twoStepLoginConfirmationTitle": {
"message": "前往网页 App 吗?"
@@ -1071,7 +1071,7 @@
"message": "主题"
},
"themeDesc": {
"message": "更改应用程序的颜色主题。"
"message": "更改应用程序的颜色主题。"
},
"themeDescAlt": {
"message": "更改应用程序的颜色主题。适用于所有已登录的账户。"
@@ -1133,7 +1133,7 @@
"message": "确认密码库导出"
},
"exportWarningDesc": {
"message": "导出的密码库数据包含未加密格式。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。用完后请立即将其删除。"
"message": "导出包含未加密格式的密码库数据。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。使用完后请立即将其删除。"
},
"encExportKeyWarningDesc": {
"message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。"
@@ -2123,7 +2123,7 @@
"message": "您仍然想要填充此登录信息吗?"
},
"autofillIframeWarning": {
"message": "该表单由不同于您保存的登录 URI 域名托管。选择「确定」自动填充,或选择「取消」停止填充。"
"message": "该表单由您保存的登录 URI 不同的域名托管。选择「确定」继续自动填充,或选择「取消」停止自动填充。"
},
"autofillIframeWarningTip": {
"message": "要防止以后出现此警告,请将此站点的 URI $HOSTNAME$ 保存到您的 Bitwarden 登录项目中。",
@@ -3982,7 +3982,7 @@
"message": "自动填充建议"
},
"autofillSuggestionsTip": {
"message": "保存此站点登录项目用来自动填充"
"message": "此站点保存为登录项目以用于自动填充"
},
"yourVaultIsEmpty": {
"message": "您的密码库是空的"
@@ -4810,7 +4810,7 @@
"message": "设置两步登录"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "从 2025 年 02 月开始Bitwarden 将向您的账户电子邮箱发送一个代码,以验证来自新设备的登录。"
"message": "从 2025 年 02 月起,当有来自新设备的登录时Bitwarden 将向您的账户电子邮箱发送验证码。"
},
"newDeviceVerificationNoticeContentPage2": {
"message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。"
@@ -4819,7 +4819,7 @@
"message": "稍后提醒我"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "您能可靠地访问您的电子邮箱 $EMAIL$ 吗?",
"message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?",
"placeholders": {
"email": {
"content": "$1",
@@ -4831,7 +4831,7 @@
"message": "不,我不能"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "是的,我可以可靠地访问我的电子邮箱"
"message": "是的,我可以正常访问我的电子邮箱"
},
"turnOnTwoStepLogin": {
"message": "开启两步登录"

View File

@@ -1,182 +1,78 @@
<ng-container *ngIf="extensionRefreshFlag">
<popup-page [loading]="loading">
<popup-header slot="header" pageTitle="{{ 'accountActions' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<ng-container *ngIf="availableAccounts$ | async as availableAccounts">
<bit-section [disableMargin]="!enableAccountSwitching">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<div *ngIf="availableAccount.isActive" [ngClass]="{ 'tw-mb-6': enableAccountSwitching }">
<auth-account
[account]="availableAccount"
[extensionRefreshFlag]="extensionRefreshFlag"
(loading)="loading = $event"
></auth-account>
</div>
<ng-container *ngIf="enableAccountSwitching">
<bit-section-header *ngIf="isFirst">
<h2 bitTypography="h6" class="tw-font-semibold">{{ "availableAccounts" | i18n }}</h2>
</bit-section-header>
<div *ngIf="!availableAccount.isActive">
<auth-account
[account]="availableAccount"
[extensionRefreshFlag]="extensionRefreshFlag"
(loading)="loading = $event"
></auth-account>
</div>
</ng-container>
</ng-container>
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</bit-section>
<popup-page [loading]="loading">
<popup-header slot="header" pageTitle="{{ 'accountActions' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<bit-section>
<bit-section-header>
<h2 bitTypography="h6" class="tw-font-semibold">
{{ "options" | i18n }}
</h2>
</bit-section-header>
<ng-container *ngIf="availableAccounts$ | async as availableAccounts">
<bit-section [disableMargin]="!enableAccountSwitching">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<div *ngIf="availableAccount.isActive" [ngClass]="{ 'tw-mb-6': enableAccountSwitching }">
<auth-account [account]="availableAccount" (loading)="loading = $event"></auth-account>
</div>
<bit-item>
<button
type="button"
bit-item-content
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="logOut(currentAccount.id)">
<i
slot="start"
class="bwi bwi-sign-out tw-text-2xl tw-text-main"
aria-hidden="true"
></i>
{{ "logOut" | i18n }}
</button>
</bit-item>
<bit-item *ngIf="showLockAll$ | async">
<button type="button" bit-item-content (click)="lockAll()">
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</bit-item>
</bit-section>
</div>
</popup-page>
</ng-container>
<ng-container *ngIf="enableAccountSwitching">
<bit-section-header *ngIf="isFirst">
<h2 bitTypography="h6" class="tw-font-semibold">{{ "availableAccounts" | i18n }}</h2>
</bit-section-header>
<ng-container *ngIf="!extensionRefreshFlag">
<app-header>
<div class="left">
<button type="button" (click)="back()">{{ "close" | i18n }}</button>
</div>
<div class="center tw-font-bold">{{ "switchAccounts" | i18n }}</div>
</app-header>
<div *ngIf="!availableAccount.isActive">
<auth-account [account]="availableAccount" (loading)="loading = $event"></auth-account>
</div>
</ng-container>
</ng-container>
<main
*ngIf="loading"
class="tw-absolute tw-z-50 tw-box-border tw-flex tw-cursor-not-allowed tw-items-center tw-justify-center tw-bg-background tw-opacity-60"
>
<i class="bwi bwi-spinner bwi-2x bwi-spin" aria-hidden="true"></i>
</main>
<main>
<div class="tw-p-2">
<div *ngIf="availableAccounts$ | async as availableAccounts">
<ul class="tw-grid tw-list-none tw-gap-2" role="listbox">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<li *ngIf="availableAccount.isActive" class="tw-mb-4" role="option">
<auth-account
[account]="availableAccount"
(loading)="loading = $event"
></auth-account>
</li>
<ng-container *ngIf="enableAccountSwitching">
<div *ngIf="isFirst" class="tw-uppercase tw-text-muted">
{{ "availableAccounts" | i18n }}
</div>
<li *ngIf="!availableAccount.isActive" role="option">
<auth-account
[account]="availableAccount"
(loading)="loading = $event"
></auth-account>
</li>
</ng-container>
</ng-container>
</ul>
<!--
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</div>
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</bit-section>
</ng-container>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<div class="tw-mb-2 tw-uppercase tw-text-muted">{{ "options" | i18n }}</div>
<div class="tw-grid tw-gap-2">
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="logOut(currentAccount.id)"
>
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="lockAll()"
*ngIf="showLockAll$ | async"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</div>
</div>
</div>
</main>
</ng-container>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<bit-section>
<bit-section-header>
<h2 bitTypography="h6" class="tw-font-semibold">
{{ "options" | i18n }}
</h2>
</bit-section-header>
<bit-item>
<button
type="button"
bit-item-content
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="logOut(currentAccount.id)">
<i slot="start" class="bwi bwi-sign-out tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
</bit-item>
<bit-item *ngIf="showLockAll$ | async">
<button type="button" bit-item-content (click)="lockAll()">
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</bit-item>
</bit-section>
</div>
</popup-page>

View File

@@ -10,9 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "@bitwarden/common/types/guid";
import {
AvatarModule,
@@ -25,7 +23,6 @@ import {
import { enableAccountSwitching } from "../../../platform/flags";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { HeaderComponent } from "../../../platform/popup/header.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@@ -44,7 +41,6 @@ import { AccountSwitcherService } from "./services/account-switcher.service";
AvatarModule,
PopupPageComponent,
PopupHeaderComponent,
HeaderComponent,
PopOutComponent,
CurrentAccountComponent,
AccountComponent,
@@ -58,7 +54,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
loading = false;
activeUserCanLock = false;
extensionRefreshFlag = false;
enableAccountSwitching = true;
constructor(
@@ -70,7 +65,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
private router: Router,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private authService: AuthService,
private configService: ConfigService,
private lockService: LockService,
) {}
@@ -109,9 +103,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
async ngOnInit() {
this.enableAccountSwitching = enableAccountSwitching();
this.extensionRefreshFlag = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
const availableVaultTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),

View File

@@ -1,109 +1,44 @@
<ng-container *ngIf="extensionRefreshFlag">
<bit-item *ngIf="account.id !== specialAccountAddId">
<button bit-item-content type="button" (click)="selectAccount(account.id)">
<bit-avatar
slot="start"
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<bit-item *ngIf="account.id !== specialAccountAddId">
<button bit-item-content type="button" (click)="selectAccount(account.id)">
<bit-avatar
slot="start"
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<span class="tw-sr-only" *ngIf="status.text === 'active'">
{{ "activeAccount" | i18n }}:
</span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<ng-container slot="secondary">
<div class="tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div class="tw-text-sm tw-italic" [attr.aria-hidden]="status.text === 'active'">
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</ng-container>
<i slot="end" class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</button>
</bit-item>
<bit-item *ngIf="account.id === specialAccountAddId">
<button type="button" bit-item-content (click)="selectAccount(account.id)">
<i slot="start" class="bwi bwi-plus tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ account.name | i18n }}
</button>
</bit-item>
</ng-container>
<ng-container *ngIf="!extensionRefreshFlag">
<button
*ngIf="account.id !== specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<div class="tw-flex-shrink-0">
<bit-avatar
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<span class="tw-sr-only" *ngIf="status.text === 'active'"> {{ "activeAccount" | i18n }}: </span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<div class="tw-text-left">
<span class="tw-sr-only" *ngIf="status.text === 'active'">
{{ "activeAccount" | i18n }}:
</span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<div class="account-switcher-row-details tw-max-w-64 tw-truncate tw-text-sm">
<ng-container slot="secondary">
<div class="tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div
class="account-switcher-row-details tw-text-sm tw-italic"
[attr.aria-hidden]="status.text === 'active'"
>
<div class="tw-text-sm tw-italic" [attr.aria-hidden]="status.text === 'active'">
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</div>
<div class="tw-ml-auto tw-flex-shrink-0">
<i class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</div>
</button>
</ng-container>
<button
*ngIf="account.id === specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<i class="bwi bwi-plus tw-text-2xl" aria-hidden="true"></i>
<div>
{{ account.name | i18n }}
</div>
<i slot="end" class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</button>
</ng-container>
</bit-item>
<bit-item *ngIf="account.id === specialAccountAddId">
<button type="button" bit-item-content (click)="selectAccount(account.id)">
<i slot="start" class="bwi bwi-plus tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ account.name | i18n }}
</button>
</bit-item>

View File

@@ -19,7 +19,6 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi
})
export class AccountComponent {
@Input() account: AvailableAccount;
@Input() extensionRefreshFlag: boolean = false;
@Output() loading = new EventEmitter<boolean>();
constructor(

View File

@@ -20,6 +20,7 @@
[showReadonlyHostname]="showReadonlyHostname"
[hideLogo]="true"
[maxWidth]="maxWidth"
[hideFooter]="hideFooter"
>
<router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet>

View File

@@ -25,6 +25,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
showAcctSwitcher?: boolean;
showBackButton?: boolean;
showLogo?: boolean;
hideFooter?: boolean;
}
@Component({
@@ -54,6 +55,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected hasLoggedInAccount: boolean = false;
protected hideFooter: boolean;
protected theme: string;
protected logo = ExtensionBitwardenLogo;
@@ -112,6 +114,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.pageIcon = firstChildRouteData["pageIcon"];
}
this.hideFooter = Boolean(firstChildRouteData["hideFooter"]);
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
@@ -158,6 +161,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.pageIcon = data.pageIcon !== null ? data.pageIcon : null;
}
if (data.hideFooter !== undefined) {
this.hideFooter = data.hideFooter !== null ? data.hideFooter : null;
}
if (data.showReadonlyHostname !== undefined) {
this.showReadonlyHostname = data.showReadonlyHostname;
}
@@ -194,6 +201,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.showBackButton = null;
this.showLogo = null;
this.maxWidth = null;
this.hideFooter = null;
}
ngOnDestroy() {

View File

@@ -1,140 +0,0 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "accountSecurity" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1" [formGroup]="form">
<div class="box list">
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
<div class="box-content single-line">
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
<input id="biometric" type="checkbox" formControlName="biometric" />
</div>
<div
class="box-content-row box-content-row-checkbox"
appBoxRow
*ngIf="supportsBiometric && this.form.value.biometric"
>
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
<input
id="autoBiometricsPrompt"
type="checkbox"
(change)="updateAutoBiometricsPrompt()"
formControlName="enableAutoBiometricsPrompt"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
<input id="pin" type="checkbox" formControlName="pin" />
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
<div class="box-content single-line">
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
<span *ngIf="policy.timeout && policy.action">
{{
"vaultTimeoutPolicyWithActionInEffect"
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
}}
</span>
<span *ngIf="policy.timeout && !policy.action">
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
</span>
<span *ngIf="!policy.timeout && policy.action">
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
</span>
</app-callout>
<app-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</app-vault-timeout-input>
<div class="box-content-row display-block" appBoxRow>
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
<select
id="vaultTimeoutAction"
name="VaultTimeoutActions"
formControlName="vaultTimeoutAction"
>
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
{{ action | i18n }}
</option>
</select>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
id="unlockMethodHelp"
class="box-footer"
>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="fingerprint()"
>
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="twoStep()"
>
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="changePassword()"
*ngIf="showChangeMasterPass"
>
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="lock()"
>
<div class="row-main">{{ "lockNow" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="!accountSwitcherEnabled"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="logOut()"
>
<div class="row-main">{{ "logOut" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@@ -1,499 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import {
BehaviorSubject,
combineLatest,
concatMap,
distinctUntilChanged,
filter,
firstValueFrom,
map,
Observable,
pairwise,
startWith,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/types/vault-timeout.type";
import { DialogService } from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsService } from "@bitwarden/key-management";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { SetPinComponent } from "../components/set-pin.component";
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
@Component({
selector: "auth-account-security",
templateUrl: "account-security-v1.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AccountSecurityComponent implements OnInit, OnDestroy {
protected readonly VaultTimeoutAction = VaultTimeoutAction;
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutOptions: VaultTimeoutOption[];
vaultTimeoutPolicyCallout: Observable<{
timeout: { hours: string; minutes: string };
action: VaultTimeoutAction;
}>;
supportsBiometric: boolean;
showChangeMasterPass = true;
accountSwitcherEnabled = false;
form = this.formBuilder.group({
vaultTimeout: [null as VaultTimeout | null],
vaultTimeoutAction: [VaultTimeoutAction.Lock],
pin: [null as boolean | null],
biometric: false,
enableAutoBiometricsPrompt: true,
});
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
public messagingService: MessagingService,
private environmentService: EnvironmentService,
private keyService: KeyService,
private stateService: StateService,
private userVerificationService: UserVerificationService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
filter((policy) => policy != null),
map((policy) => {
let timeout;
if (policy.data?.minutes) {
timeout = {
hours: Math.floor(policy.data?.minutes / 60).toString(),
minutes: (policy.data?.minutes % 60).toString(),
};
}
return { timeout: timeout, action: policy.data?.action };
}),
);
const showOnLocked =
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
this.vaultTimeoutOptions = [
{ name: this.i18nService.t("immediately"), value: 0 },
{ name: this.i18nService.t("oneMinute"), value: 1 },
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
{ name: this.i18nService.t("oneHour"), value: 60 },
{ name: this.i18nService.t("fourHours"), value: 240 },
];
if (showOnLocked) {
this.vaultTimeoutOptions.push({
name: this.i18nService.t("onLocked"),
value: VaultTimeoutStringType.OnLocked,
});
}
this.vaultTimeoutOptions.push({
name: this.i18nService.t("onRestart"),
value: VaultTimeoutStringType.OnRestart,
});
this.vaultTimeoutOptions.push({
name: this.i18nService.t("never"),
value: VaultTimeoutStringType.Never,
});
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
let timeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
);
if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) {
timeout = VaultTimeoutStringType.OnRestart;
}
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
),
pin: await this.pinService.isPinSet(activeAccount.id),
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: await firstValueFrom(
this.biometricStateService.promptAutomatically$,
),
};
this.form.patchValue(initialValues, { emitEvent: false });
this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.vaultTimeout.valueChanges
.pipe(
startWith(initialValues.vaultTimeout), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeout(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.vaultTimeoutAction.valueChanges
.pipe(
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeoutAction(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.pin.valueChanges
.pipe(
concatMap(async (value) => {
await this.updatePin(value);
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.biometric.valueChanges
.pipe(
distinctUntilChanged(),
concatMap(async (enabled) => {
await this.updateBiometric(enabled);
if (enabled) {
this.form.controls.enableAutoBiometricsPrompt.enable();
} else {
this.form.controls.enableAutoBiometricsPrompt.disable();
}
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, action]) => {
this.availableVaultTimeoutActions = availableActions;
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
// NOTE: The UI doesn't properly update without detect changes.
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
// meaning that we are forced to use regular class variables instead of observables.
this.changeDetectorRef.detectChanges();
});
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
maximumVaultTimeoutPolicy,
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, policy]) => {
if (policy?.data?.action || availableActions.length <= 1) {
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else {
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
}
});
}
async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
if (newValue === VaultTimeoutStringType.Never) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
content: { key: "neverLockWarning" },
type: "warning",
});
if (!confirmed) {
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
return;
}
}
// The minTimeoutError does not apply to browser because it supports Immediately
// So only check for the policyError
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
return;
}
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
newValue,
vaultTimeoutAction,
);
if (newValue === VaultTimeoutStringType.Never) {
this.messagingService.send("bgReseedStorage");
}
}
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
if (newValue === VaultTimeoutAction.LogOut) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
content: { key: "vaultTimeoutLogOutConfirmation" },
type: "warning",
});
if (!confirmed) {
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
emitEvent: false,
});
return;
}
}
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
return;
}
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
this.form.value.vaultTimeout,
newValue,
);
this.refreshTimeoutSettings$.next();
}
async updatePin(value: boolean) {
if (value) {
const dialogRef = SetPinComponent.open(this.dialogService);
if (dialogRef == null) {
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}
const userHasPinSet = await firstValueFrom(dialogRef.closed);
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
} else {
await this.vaultTimeoutSettingsService.clear();
}
}
async updateBiometric(enabled: boolean) {
if (enabled && this.supportsBiometric) {
let granted;
try {
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
} catch (e) {
// eslint-disable-next-line
console.error(e);
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
await this.dialogService.openSimpleDialog({
title: { key: "nativeMessaginPermissionSidebarTitle" },
content: { key: "nativeMessaginPermissionSidebarDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "info",
});
this.form.controls.biometric.setValue(false);
return;
}
}
if (!granted) {
await this.dialogService.openSimpleDialog({
title: { key: "nativeMessaginPermissionErrorTitle" },
content: { key: "nativeMessaginPermissionErrorDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
this.form.controls.biometric.setValue(false);
return;
}
const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed);
await this.keyService.refreshAdditionalKeys();
await Promise.race([
awaitDesktopDialogClosed.then(async (result) => {
if (result !== true) {
this.form.controls.biometric.setValue(false);
}
}),
this.biometricsService
.authenticateBiometric()
.then((result) => {
this.form.controls.biometric.setValue(result);
if (!result) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorEnableBiometricTitle"),
this.i18nService.t("errorEnableBiometricDesc"),
);
}
})
.catch((e) => {
// Handle connection errors
this.form.controls.biometric.setValue(false);
const error = BiometricErrors[e.message as BiometricErrorTypes];
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dialogService.openSimpleDialog({
title: { key: error.title },
content: { key: error.description },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
})
.finally(() => {
awaitDesktopDialogRef.close(true);
}),
]);
} else {
await this.biometricStateService.setBiometricUnlockEnabled(false);
await this.biometricStateService.setFingerprintValidated(false);
}
}
async updateAutoBiometricsPrompt() {
await this.biometricStateService.setPromptAutomatically(
this.form.value.enableAutoBiometricsPrompt,
);
}
async changePassword() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "continueToWebApp" },
content: { key: "changeMasterPasswordOnWebConfirmation" },
type: "info",
acceptButtonText: { key: "continue" },
});
if (confirmed) {
const env = await firstValueFrom(this.environmentService.environment$);
await BrowserApi.createNewTab(env.getWebVaultUrl());
}
}
async twoStep() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "twoStepLogin" },
content: { key: "twoStepLoginConfirmation" },
type: "info",
});
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("https://bitwarden.com/help/setup-two-step-login/");
}
}
async fingerprint() {
const fingerprint = await this.keyService.getFingerprint(await this.stateService.getUserId());
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
fingerprint,
});
return firstValueFrom(dialogRef.closed);
}
async lock() {
await this.vaultTimeoutService.lock();
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
type: "info",
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (confirmed) {
this.messagingService.send("logout", { userId: userId });
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -60,7 +60,6 @@ import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { SetPinComponent } from "../components/set-pin.component";
@@ -82,7 +81,6 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
JslibModule,
LinkModule,
PopOutComponent,
PopupFooterComponent,
PopupHeaderComponent,
PopupPageComponent,
RouterModule,

View File

@@ -60,10 +60,18 @@ describe("NotificationBackground", () => {
const configService = mock<ConfigService>();
const accountService = mock<AccountService>();
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
id: "testId" as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
});
beforeEach(() => {
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Locked);
authService = mock<AuthService>();
authService.activeAccountStatus$ = activeAccountStatusMock$;
accountService.activeAccount$ = activeAccountSubject;
notificationBackground = new NotificationBackground(
autofillService,
cipherService,
@@ -683,13 +691,6 @@ describe("NotificationBackground", () => {
});
describe("saveOrUpdateCredentials", () => {
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
id: "testId" as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
});
let getDecryptedCipherByIdSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
let updatePasswordSpy: jest.SpyInstance;

View File

@@ -83,6 +83,8 @@ export default class NotificationBackground {
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
};
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private autofillService: AutofillService,
private cipherService: CipherService,
@@ -569,9 +571,7 @@ export default class NotificationBackground {
return;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(newCipher, activeUserId);
try {
@@ -611,10 +611,7 @@ export default class NotificationBackground {
return;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(cipherView, activeUserId);
try {
// We've only updated the password, no need to broadcast editedCipher message
@@ -647,17 +644,15 @@ export default class NotificationBackground {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false;
}
const folders = await firstValueFrom(this.folderService.folderViews$);
const activeUserId = await firstValueFrom(this.activeUserId$);
const folders = await firstValueFrom(this.folderService.folderViews$(activeUserId));
return folders.some((x) => x.id === folderId);
}
private async getDecryptedCipherById(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.type === CipherType.Login) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
@@ -697,7 +692,8 @@ export default class NotificationBackground {
* Returns the first value found from the folder service's folderViews$ observable.
*/
private async getFolderData() {
return await firstValueFrom(this.folderService.folderViews$);
const activeUserId = await firstValueFrom(this.activeUserId$);
return await firstValueFrom(this.folderService.folderViews$(activeUserId));
}
private async getWebVaultUrl(): Promise<string> {

View File

@@ -2275,6 +2275,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
card,
identity,
sender,
addNewCipherType,
}: CurrentAddNewItemData) {
const cipherView: CipherView = this.buildNewVaultItemCipherView({
login,
@@ -2294,7 +2295,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
collectionIds: cipherView.collectionIds,
});
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
await this.openAddEditVaultItemPopout(sender.tab, {
cipherId: cipherView.id,
cipherType: addNewCipherType,
});
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
} catch (error) {
this.logService.error("Error building cipher and opening add/edit vault item popout", error);

View File

@@ -458,7 +458,7 @@ export class BrowserApi {
// and that prompts us to show a new tab, this apparently doesn't happen on sideloaded
// extensions and only shows itself production scenarios. See: https://bitwarden.atlassian.net/browse/PM-12298
if (this.isSafariApi) {
self.location.reload();
return self.location.reload();
}
return chrome.runtime.reload();
}

View File

@@ -3,7 +3,6 @@ import { Subject } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -30,12 +29,12 @@ describe("ForegroundSyncService", () => {
const cipherService = mock<CipherService>();
const collectionService = mock<CollectionService>();
const apiService = mock<ApiService>();
const accountService = mock<AccountService>();
const accountService = mockAccountServiceWith(userId);
const authService = mock<AuthService>();
const sendService = mock<InternalSendService>();
const sendApiService = mock<SendApiService>();
const messageListener = mock<MessageListener>();
const stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
const stateProvider = new FakeStateProvider(accountService);
const sut = new ForegroundSyncService(
stateService,

View File

@@ -65,7 +65,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
@@ -351,11 +350,12 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, {
{
path: "account-security",
component: AccountSecurityComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
path: "notifications",
canActivate: [authGuard],
@@ -713,6 +713,7 @@ const routes: Routes = [
pageTitle: {
key: "importantNotice",
},
hideFooter: true,
},
},
{

View File

@@ -29,7 +29,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
@@ -165,7 +164,6 @@ import "../platform/popup/locales";
TwoFactorOptionsComponent,
UpdateTempPasswordComponent,
UserVerificationComponent,
AccountSecurityComponentV1,
VaultTimeoutInputComponent,
ViewComponent,
ViewCustomFieldsComponent,

View File

@@ -171,7 +171,7 @@ describe("AddEditFolderDialogComponent", () => {
it("deletes the folder", async () => {
await component.deleteFolder();
expect(deleteFolder).toHaveBeenCalledWith(folderView.id);
expect(deleteFolder).toHaveBeenCalledWith(folderView.id, "");
expect(showToast).toHaveBeenCalledWith({
variant: "success",
title: null,

View File

@@ -13,7 +13,7 @@ import {
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -67,6 +67,7 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
name: ["", Validators.required],
});
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
private destroyRef = inject(DestroyRef);
constructor(
@@ -114,10 +115,10 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
this.folder.name = this.folderForm.controls.name.value;
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const activeUserId = await firstValueFrom(this.activeUserId$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const folder = await this.folderService.encrypt(this.folder, userKey);
await this.folderApiService.save(folder);
await this.folderApiService.save(folder, activeUserId);
this.toastService.showToast({
variant: "success",
@@ -144,7 +145,8 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
}
try {
await this.folderApiService.delete(this.folder.id);
const activeUserId = await firstValueFrom(this.activeUserId$);
await this.folderApiService.delete(this.folder.id, activeUserId);
this.toastService.showToast({
variant: "success",
title: null,

View File

@@ -1,4 +1,5 @@
<bit-search
autocomplete="off"
[placeholder]="'search' | i18n"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()"

View File

@@ -7,9 +7,12 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -32,7 +35,7 @@ describe("VaultPopupListFiltersService", () => {
} as unknown as CollectionService;
const folderService = {
folderViews$,
folderViews$: () => folderViews$,
} as unknown as FolderService;
const cipherService = {
@@ -60,6 +63,8 @@ describe("VaultPopupListFiltersService", () => {
policyAppliesToActiveUser$.next(false);
policyService.policyAppliesToActiveUser$.mockClear();
const accountService = mockAccountServiceWith("userId" as UserId);
collectionService.getAllNested = () => Promise.resolve([]);
TestBed.configureTestingModule({
providers: [
@@ -92,6 +97,10 @@ describe("VaultPopupListFiltersService", () => {
useValue: { getGlobal: () => ({ state$, update }) },
},
{ provide: FormBuilder, useClass: FormBuilder },
{
provide: AccountService,
useValue: accountService,
},
],
});

View File

@@ -20,6 +20,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -102,6 +103,8 @@ export class VaultPopupListFiltersService {
map((ciphers) => Object.values(ciphers)),
);
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private folderService: FolderService,
private cipherService: CipherService,
@@ -111,6 +114,7 @@ export class VaultPopupListFiltersService {
private formBuilder: FormBuilder,
private policyService: PolicyService,
private stateProvider: StateProvider,
private accountService: AccountService,
) {
this.filterForm.controls.organization.valueChanges
.pipe(takeUntilDestroyed())
@@ -264,61 +268,68 @@ export class VaultPopupListFiltersService {
/**
* Folder array structured to be directly passed to `ChipSelectComponent`
*/
folders$: Observable<ChipSelectOption<FolderView>[]> = combineLatest([
this.filters$.pipe(
distinctUntilChanged(
(previousFilter, currentFilter) =>
// Only update the collections when the organizationId filter changes
previousFilter.organization?.id === currentFilter.organization?.id,
folders$: Observable<ChipSelectOption<FolderView>[]> = this.activeUserId$.pipe(
switchMap((userId) =>
combineLatest([
this.filters$.pipe(
distinctUntilChanged(
(previousFilter, currentFilter) =>
// Only update the collections when the organizationId filter changes
previousFilter.organization?.id === currentFilter.organization?.id,
),
),
this.folderService.folderViews$(userId),
this.cipherViews$,
]).pipe(
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
if (folders.length === 1 && folders[0].id === null) {
// Do not display folder selections when only the "no folder" option is available.
return [filters, [], cipherViews];
}
// Sort folders by alphabetic name
folders.sort(Utils.getSortFunction(this.i18nService, "name"));
let arrangedFolders = folders;
const noFolder = folders.find((f) => f.id === null);
if (noFolder) {
// Update `name` of the "no folder" option to "Items with no folder"
const updatedNoFolder = {
...noFolder,
name: this.i18nService.t("itemsWithNoFolder"),
};
// Move the "no folder" option to the end of the list
arrangedFolders = [...folders.filter((f) => f.id !== null), updatedNoFolder];
}
return [filters, arrangedFolders, cipherViews];
}),
map(([filters, folders, cipherViews]) => {
const organizationId = filters.organization?.id ?? null;
// When no org or "My vault" is selected, return all folders
if (organizationId === null || organizationId === MY_VAULT_ID) {
return folders;
}
const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId);
// Return only the folders that have ciphers within the filtered organization
return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id));
}),
map((folders) => {
const nestedFolders = this.getAllFoldersNested(folders);
return new DynamicTreeNode<FolderView>({
fullList: folders,
nestedList: nestedFolders,
});
}),
map((folders) =>
folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")),
),
),
),
this.folderService.folderViews$,
this.cipherViews$,
]).pipe(
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
if (folders.length === 1 && folders[0].id === null) {
// Do not display folder selections when only the "no folder" option is available.
return [filters, [], cipherViews];
}
// Sort folders by alphabetic name
folders.sort(Utils.getSortFunction(this.i18nService, "name"));
let arrangedFolders = folders;
const noFolder = folders.find((f) => f.id === null);
if (noFolder) {
// Update `name` of the "no folder" option to "Items with no folder"
noFolder.name = this.i18nService.t("itemsWithNoFolder");
// Move the "no folder" option to the end of the list
arrangedFolders = [...folders.filter((f) => f.id !== null), noFolder];
}
return [filters, arrangedFolders, cipherViews];
}),
map(([filters, folders, cipherViews]) => {
const organizationId = filters.organization?.id ?? null;
// When no org or "My vault" is selected, return all folders
if (organizationId === null || organizationId === MY_VAULT_ID) {
return folders;
}
const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId);
// Return only the folders that have ciphers within the filtered organization
return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id));
}),
map((folders) => {
const nestedFolders = this.getAllFoldersNested(folders);
return new DynamicTreeNode<FolderView>({
fullList: folders,
nestedList: nestedFolders,
});
}),
map((folders) =>
folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")),
),
);
/**

View File

@@ -4,10 +4,13 @@ import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { DialogService } from "@bitwarden/components";
@@ -52,8 +55,9 @@ describe("FoldersV2Component", () => {
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: FolderService, useValue: { folderViews$ } },
{ provide: FolderService, useValue: { folderViews$: () => folderViews$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
],
})
.overrideComponent(FoldersV2Component, {

View File

@@ -1,8 +1,10 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { map, Observable } from "rxjs";
import { filter, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
@@ -45,18 +47,21 @@ export class FoldersV2Component {
folders$: Observable<FolderView[]>;
NoFoldersIcon = VaultIcons.NoFolders;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private folderService: FolderService,
private dialogService: DialogService,
private accountService: AccountService,
) {
this.folders$ = this.folderService.folderViews$.pipe(
this.folders$ = this.activeUserId$.pipe(
filter((userId): userId is UserId => userId !== null),
switchMap((userId) => this.folderService.folderViews$(userId)),
map((folders) => {
// Remove the last folder, which is the "no folder" option folder
if (folders.length > 0) {
return folders.slice(0, folders.length - 1);
}
return folders;
}),
);

View File

@@ -1,7 +1,9 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { map, Observable } from "rxjs";
import { filter, map, Observable, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@@ -12,16 +14,21 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
export class FoldersComponent {
folders$: Observable<FolderView[]>;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private folderService: FolderService,
private router: Router,
private accountService: AccountService,
) {
this.folders$ = this.folderService.folderViews$.pipe(
this.folders$ = this.activeUserId$.pipe(
filter((userId): userId is UserId => userId != null),
switchMap((userId) => this.folderService.folderViews$(userId)),
map((folders) => {
// Remove the last folder, which is the "no folder" option folder
if (folders.length > 0) {
folders = folders.slice(0, folders.length - 1);
return folders.slice(0, folders.length - 1);
}
return folders;
}),
);

View File

@@ -24,7 +24,7 @@ export class VaultFilterService extends BaseVaultFilterService {
collectionService: CollectionService,
policyService: PolicyService,
stateProvider: StateProvider,
private accountService: AccountService,
accountService: AccountService,
) {
super(
organizationService,
@@ -33,6 +33,7 @@ export class VaultFilterService extends BaseVaultFilterService {
collectionService,
policyService,
stateProvider,
accountService,
);
this.vaultFilter.myVaultOnly = false;
this.vaultFilter.selectedOrganizationId = null;

View File

@@ -118,58 +118,58 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden - wachtwoordbeheerder</value>
<value>Bitwarden Wachtwoordbeheerder</value>
</data>
<data name="Summary" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>Thuis, op het werk of onderweg, Bitwarden beveiligt eenvoudig al je wachtwoorden, sleutels en gevoelige informatie.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
<value>Erkend als de beste wachtwoordmanager door PCMag, WIRED, The Verge, CNET, G2 en meer!
SECURE YOUR DIGITAL LIFE
Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access.
BEVEILIG JE DIGITALE LEVEN
Beveilig je digitale leven en bescherm je tegen datalekken door unieke, sterke wachtwoorden te genereren en op te slaan voor elke account. Bewaar alles in een end-to-end versleutelde wachtwoordkluis waar alleen jij toegang toe hebt.
ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions.
OVERAL EN ALTIJD TOEGANG TOT JE GEGEVENS, OP ELK APPARAAT
Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten zonder beperkingen.
EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE
Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
IEDEREEN ZOU DE MIDDELEN MOETEN HEBBEN OM VEILIG ONLINE TE BLIJVEN
Gebruik Bitwarden gratis, zonder advertenties of verkoop van gegevens. Bitwarden vindt dat iedereen de mogelijkheid moet hebben om veilig online te zijn. Premium abonnementen bieden toegang tot geavanceerde functies.
EMPOWER YOUR TEAMS WITH BITWARDEN
Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more.
VERSTERK JE TEAMS MET BITWARDEN
Abonnementen voor Teams en Enterprise worden geleverd met professionele zakelijke functies. Enkele voorbeelden zijn SSO-integratie, zelf hosten, directory-integratie en SCIM provisioning, globaal beleid, API-toegang, gebeurtenislogboeken en meer.
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
Gebruik Bitwarden om je medewerkers te beveiligen en gevoelige informatie te delen met collega's.
More reasons to choose Bitwarden:
Meer redenen om voor Bitwarden te kiezen:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private.
Encryptie van wereldklasse
Wachtwoorden worden beschermd met geavanceerde end-to-end versleuteling (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat je gegevens veilig en privé blijven.
3rd-party Audits
Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications.
Audits door derde partijen
Bitwarden voert regelmatig uitgebreide beveiligingsaudits uit bij gerenommeerde beveiligingsbedrijven. Deze jaarlijkse audits omvatten broncodebeoordelingen en penetratietests voor Bitwarden IP's, servers en webapplicaties.
Advanced 2FA
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
Geavanceerde 2FA
Beveilig je login met een authenticator van derden, codes per e-mail of FIDO2 WebAuthn referenties zoals een hardware beveiligingssleutel of passkey.
Bitwarden Send
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
Verstuur gegevens rechtstreeks naar anderen met behoud van end-to-end versleutelde beveiliging en beperking van blootstelling.
Built-in Generator
Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy.
Ingebouwde generator
Maak lange, complexe en duidelijke wachtwoorden en unieke gebruikersnamen voor elke site die je bezoekt. Integreer met e-mail alias providers voor extra privacy.
Global Translations
Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin.
Wereldwijde vertalingen
Bitwarden vertalingen bestaan voor meer dan 60 talen, vertaald door de wereldwijde gemeenschap via Crowdin.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Platformoverkoepelende applicaties
Beveilig en deel gevoelige gegevens in je Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop OS, en meer.
Bitwarden secures more than just passwords
End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
Bitwarden beveiligt meer dan alleen wachtwoorden
Met de end-to-end versleutelde oplossingen voor referentiebeheer van Bitwarden kunnen organisaties alles beveiligen, inclusief ontwikkelaarsgeheimen en ervaringen met wachtwoorden. Bezoek Bitwarden.com voor meer informatie over Bitwarden Secrets Manager en Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>Thuis, op het werk of onderweg, Bitwarden beveiligt eenvoudig al je wachtwoorden, sleutels en gevoelige informatie.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Synchroniseer en gebruik je kluis op meerdere apparaten</value>

View File

@@ -24,6 +24,8 @@ import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response";
export class EditCommand {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -121,12 +123,12 @@ export class EditCommand {
cipher.collectionIds = req;
try {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
await this.cipherService.getKeyForCipherKeyDecryption(
updatedCipher,
await firstValueFrom(this.activeUserId$),
),
);
const res = new CipherResponse(decCipher);
return Response.success(res);
@@ -136,7 +138,8 @@ export class EditCommand {
}
private async editFolder(id: string, req: FolderExport) {
const folder = await this.folderService.getFromState(id);
const activeUserId = await firstValueFrom(this.activeUserId$);
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) {
return Response.notFound();
}
@@ -144,12 +147,11 @@ export class EditCommand {
let folderView = await folder.decrypt();
folderView = FolderExport.toView(req, folderView);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const encFolder = await this.folderService.encrypt(folderView, userKey);
try {
await this.folderApiService.save(encFolder);
const updatedFolder = await this.folderService.get(folder.id);
await this.folderApiService.save(encFolder, activeUserId);
const updatedFolder = await this.folderService.get(folder.id, activeUserId);
const decFolder = await updatedFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);

View File

@@ -51,6 +51,8 @@ import { FolderResponse } from "../vault/models/folder.response";
import { DownloadCommand } from "./download.command";
export class GetCommand extends DownloadCommand {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -113,10 +115,8 @@ export class GetCommand extends DownloadCommand {
let decCipher: CipherView = null;
if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (cipher != null) {
const activeUserId = await firstValueFrom(this.activeUserId$);
decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
@@ -383,13 +383,14 @@ export class GetCommand extends DownloadCommand {
private async getFolder(id: string) {
let decFolder: FolderView = null;
const activeUserId = await firstValueFrom(this.activeUserId$);
if (Utils.isGuid(id)) {
const folder = await this.folderService.getFromState(id);
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder != null) {
decFolder = await folder.decrypt();
}
} else if (id.trim() !== "") {
let folders = await this.folderService.getAllDecryptedFromState();
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
folders = CliUtils.searchFolders(folders, id);
if (folders.length > 1) {
return Response.multipleResults(folders.map((f) => f.id));
@@ -551,9 +552,7 @@ export class GetCommand extends DownloadCommand {
private async getFingerprint(id: string) {
let fingerprint: string[] = null;
if (id === "me") {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
} else if (Utils.isGuid(id)) {

View File

@@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import {
OrganizationUserApiService,
@@ -12,6 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -38,6 +39,7 @@ export class ListCommand {
private organizationUserApiService: OrganizationUserApiService,
private apiService: ApiService,
private eventCollectionService: EventCollectionService,
private accountService: AccountService,
) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -135,7 +137,10 @@ export class ListCommand {
}
private async listFolders(options: Options) {
let folders = await this.folderService.getAllDecryptedFromState();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
if (options.search != null && options.search.trim() !== "") {
folders = CliUtils.searchFolders(folders, options.search);

View File

@@ -76,6 +76,7 @@ export class OssServeConfigurator {
this.serviceContainer.organizationUserApiService,
this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService,
this.serviceContainer.accountService,
);
this.createCommand = new CreateCommand(
this.serviceContainer.cipherService,
@@ -115,6 +116,7 @@ export class OssServeConfigurator {
this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.accountService,
);
this.confirmCommand = new ConfirmCommand(
this.serviceContainer.apiService,

View File

@@ -113,6 +113,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.organizationUserApiService,
this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService,
this.serviceContainer.accountService,
);
const response = await command.run(object, cmd);
@@ -321,6 +322,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.accountService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);

View File

@@ -30,6 +30,8 @@ import { CipherResponse } from "./models/cipher.response";
import { FolderResponse } from "./models/folder.response";
export class CreateCommand {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -86,9 +88,7 @@ export class CreateCommand {
}
private async createCipher(req: CipherExport) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try {
const newCipher = await this.cipherService.createWithServer(cipher);
@@ -152,9 +152,7 @@ export class CreateCommand {
}
try {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const updatedCipher = await this.cipherService.saveAttachmentRawWithServer(
cipher,
fileName,
@@ -171,12 +169,12 @@ export class CreateCommand {
}
private async createFolder(req: FolderExport) {
const activeAccountId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeAccountId.id);
const activeUserId = await firstValueFrom(this.activeUserId$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
try {
await this.folderApiService.save(folder);
const newFolder = await this.folderService.get(folder.id);
await this.folderApiService.save(folder, activeUserId);
const newFolder = await this.folderService.get(folder.id, activeUserId);
const decFolder = await newFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);

View File

@@ -1,6 +1,7 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -19,6 +20,7 @@ export class DeleteCommand {
private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
private cipherAuthorizationService: CipherAuthorizationService,
private accountService: AccountService,
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -103,13 +105,16 @@ export class DeleteCommand {
}
private async deleteFolder(id: string) {
const folder = await this.folderService.getFromState(id);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) {
return Response.notFound();
}
try {
await this.folderApiService.delete(id);
await this.folderApiService.delete(id, activeUserId);
return Response.success();
} catch (e) {
return Response.error(e);

View File

@@ -567,9 +567,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.2"
version = "1.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
dependencies = [
"shlex",
]
@@ -616,9 +616,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.22"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
@@ -626,9 +626,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.22"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
@@ -650,9 +650,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clipboard-win"
@@ -703,16 +703,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
@@ -740,9 +730,9 @@ dependencies = [
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@@ -759,9 +749,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
@@ -821,9 +811,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef"
checksum = "4d44ff199ff93242c3afe480ab588d544dd08d72e92885e152ffebc670f076ad"
dependencies = [
"cc",
"cxxbridge-cmd",
@@ -835,9 +825,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c"
checksum = "66fd8f17ad454fc1e4f4ab83abffcc88a532e90350d3ffddcb73030220fcbd52"
dependencies = [
"cc",
"codespan-reporting",
@@ -849,9 +839,9 @@ dependencies = [
[[package]]
name = "cxxbridge-cmd"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6"
checksum = "4717c9c806a9e07fdcb34c84965a414ea40fafe57667187052cf1eb7f5e8a8a9"
dependencies = [
"clap",
"codespan-reporting",
@@ -862,15 +852,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa"
checksum = "2f6515329bf3d98f4073101c7866ff2bec4e635a13acb82e3f3753fff0bf43cb"
[[package]]
name = "cxxbridge-macro"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95"
checksum = "fb93e6a7ce8ec985c02bbb758237a31598b340acbbc3c19c5a4fa6adaaac92ab"
dependencies = [
"proc-macro2",
"quote",
@@ -935,7 +925,7 @@ dependencies = [
"bitwarden-russh",
"byteorder",
"cbc",
"core-foundation 0.10.0",
"core-foundation",
"desktop_objc",
"dirs",
"ed25519",
@@ -996,7 +986,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"cc",
"core-foundation 0.9.4",
"core-foundation",
"glob",
"thiserror",
"tokio",
@@ -1144,7 +1134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1176,9 +1166,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
@@ -1518,9 +1508,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.162"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libloading"
@@ -1529,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -1643,9 +1633,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
dependencies = [
"adler2",
]
@@ -1677,9 +1667,9 @@ dependencies = [
[[package]]
name = "napi-build"
version = "2.1.3"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
@@ -2375,9 +2365,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redox_syscall"
version = "0.5.7"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
]
@@ -2479,15 +2469,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.41"
version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2562,12 +2552,12 @@ dependencies = [
[[package]]
name = "security-framework"
version = "3.0.0"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc"
dependencies = [
"bitflags",
"core-foundation 0.10.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -2575,9 +2565,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.0"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
dependencies = [
"core-foundation-sys",
"libc",
@@ -2585,27 +2575,27 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
@@ -2846,7 +2836,7 @@ dependencies = [
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3350,7 +3340,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -31,7 +31,7 @@ base64 = "=0.22.1"
byteorder = "=1.5.0"
cbc = { version = "=0.1.2", features = ["alloc"] }
homedir = "=0.3.4"
libc = "=0.2.162"
libc = "=0.2.169"
pin-project = "=1.1.7"
dirs = "=5.0.1"
futures = "=0.3.31"
@@ -81,8 +81,8 @@ keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { version = "=0.10.0", optional = true }
security-framework = { version = "=3.0.0", optional = true }
security-framework-sys = { version = "=2.12.0", optional = true }
security-framework = { version = "=3.1.0", optional = true }
security-framework-sys = { version = "=2.13.0", optional = true }
desktop_objc = { path = "../objc" }
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -21,10 +21,10 @@ serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
tokio = { version = "1.39.2", features = ["sync"] }
tokio-util = "0.7.11"
uniffi = { version = "0.28.0", features = ["cli"] }
uniffi = { version = "0.28.3", features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
oslog = "0.2.0"
[build-dependencies]
uniffi = { version = "0.28.0", features = ["build"] }
uniffi = { version = "0.28.3", features = ["build"] }

View File

@@ -30,4 +30,4 @@ tokio-stream = "=0.1.15"
windows-registry = "=0.3.0"
[build-dependencies]
napi-build = "=2.1.3"
napi-build = "=2.1.4"

View File

@@ -14,8 +14,8 @@ thiserror = "=1.0.69"
tokio = "1.39.1"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "=0.9.4"
core-foundation = "=0.10.0"
[build-dependencies]
cc = "1.0.104"
cc = "1.2.4"
glob = "0.3.1"

View File

@@ -133,7 +133,7 @@
"entitlements": "resources/entitlements.mas.plist",
"entitlementsInherit": "resources/entitlements.mas.inherit.plist",
"entitlementsLoginHelper": "resources/entitlements.mas.loginhelper.plist",
"hardenedRuntime": false,
"hardenedRuntime": true,
"extendInfo": {
"LSMinimumSystemVersion": "12",
"ElectronTeamID": "LTZ2PFU5D6"

View File

@@ -6,5 +6,7 @@
<true/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

View File

@@ -8,5 +8,7 @@
<array>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
</array>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

View File

@@ -4,10 +4,6 @@
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!--
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>

View File

@@ -6,9 +6,7 @@
<true/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!--
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>

View File

@@ -34,5 +34,7 @@
<string>/Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/</string>
<string>/Library/Application Support/Vivaldi/NativeMessagingHosts/</string>
</array>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

View File

@@ -3399,10 +3399,10 @@
"message": "ملاحظة هامة"
},
"setupTwoStepLogin": {
"message": "إعداد المصادقة الثنائية"
"message": "إعداد تسجيل الدخول بخطوتين"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "سيقوم Bitwarden بإرسال رمز إلى البريد الإلكتروني الخاص بحسابك للتحقق من تسجيلات الدخول من الأجهزة الجديدة ابتداء من فبراير 2025."
"message": "سيقوم Bitwarden بإرسال رمز إلى البريد الإلكتروني الخاص بحسابك للتحقق من تسجيلات الدخول من الأجهزة الجديدة ابتداءً من فبراير 2025."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "يمكنك إعداد المصادقة الثنائية كطريقة بديلة لحماية حسابك أو تغيير بريدك الإلكتروني إلى بريد يمكنك الوصول إليه."
@@ -3426,7 +3426,7 @@
"message": "نعم، يمكنني الوصول بشكل موثوق إلى بريدي الإلكتروني"
},
"turnOnTwoStepLogin": {
"message": فعيل المصادقة الثنائية"
"message": شغيل تسجيل الدخول بخطوتين"
},
"changeAcctEmail": {
"message": "تغيير البريد الإلكتروني الخاص بالحساب"

View File

@@ -2729,7 +2729,7 @@
"message": "Laitteeseesi lähetettiin ilmoitus"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen"
},
"needAnotherOptionV1": {
"message": "Tarvitsetko toisen vaihtoehdon?"
@@ -3402,7 +3402,7 @@
"message": "Määritä kaksivaiheinen kirjautuminen"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."

View File

@@ -323,7 +323,7 @@
"message": "Générer un mot de passe"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Générer une phrase de passe"
},
"type": {
"message": "Type"
@@ -467,7 +467,7 @@
"message": "Copier la clé privée SSH"
},
"copyPassphrase": {
"message": "Copy passphrase",
"message": "Copier la phrase de passe",
"description": "Copy passphrase to clipboard"
},
"copyUri": {
@@ -926,7 +926,7 @@
"message": "La session d'authentification a expiré. Veuillez redémarrer le processus de connexion."
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "URL du serveur auto-hébergé",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@@ -1951,7 +1951,7 @@
"message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité."
},
"receiveMarketingEmailsV2": {
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox."
"message": "Obtenez des conseils, des annonces et des opportunités de recherche de la part de Bitwarden dans votre boîte de réception."
},
"unsubscribe": {
"message": "Se désabonner"
@@ -2478,10 +2478,10 @@
"message": "Générer le nom d'utilisateur"
},
"generateEmail": {
"message": "Generate email"
"message": "Générer un courriel"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"message": "La valeur doit être comprise entre $MIN$ et $MAX$.",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@@ -2495,7 +2495,7 @@
}
},
"passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
"message": " Utilisez $RECOMMENDED$ caractères ou plus pour générer un mot de passe fort.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2505,7 +2505,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
"message": " Utilisez $RECOMMENDED$ mots ou plus pour générer une phrase de passe forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2729,10 +2729,10 @@
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte digitale correspond à celle de l'autre appareil"
},
"needAnotherOptionV1": {
"message": "Need another option?"
"message": "Besoin d'une autre option ?"
},
"fingerprintMatchInfo": {
"message": "Veuillez vous assurer que votre coffre est déverrouillé et que la phrase d'empreinte correspond à celle de l'autre appareil."
@@ -2741,13 +2741,13 @@
"message": "Phrase d'empreinte"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
"message": "Vous serez notifié une fois que la demande sera approuvée"
},
"needAnotherOption": {
"message": "La connexion avec l'appareil doit être configurée dans les paramètres de l'application Bitwarden. Besoin d'une autre option ?"
},
"viewAllLogInOptions": {
"message": "View all log in options"
"message": "Afficher toutes les options de connexion"
},
"viewAllLoginOptions": {
"message": "Afficher toutes les options de connexion"

View File

@@ -27,7 +27,7 @@
"message": "Nota sicura"
},
"typeSshKey": {
"message": "SSH key"
"message": "Chiave SSH"
},
"folders": {
"message": "Cartelle"
@@ -64,7 +64,7 @@
}
},
"welcomeBack": {
"message": "Welcome back"
"message": "Bentornato"
},
"moveToOrgDesc": {
"message": "Scegli un'organizzazione in cui vuoi spostare questo elemento. Spostarlo in un'organizzazione trasferisce la proprietà dell'elemento all'organizzazione. Una volta spostato, non sarai più il proprietario diretto di questo elemento."
@@ -181,61 +181,61 @@
"message": "Indirizzo"
},
"sshPrivateKey": {
"message": "Private key"
"message": "Chiave privata"
},
"sshPublicKey": {
"message": "Public key"
"message": "Chiave pubblica"
},
"sshFingerprint": {
"message": "Fingerprint"
"message": "Impronta digitale"
},
"sshKeyAlgorithm": {
"message": "Key type"
"message": "Tipo di chiave"
},
"sshKeyAlgorithmED25519": {
"message": "ED25519"
},
"sshKeyAlgorithmRSA2048": {
"message": "RSA 2048-Bit"
"message": "RSA a 2048 bit"
},
"sshKeyAlgorithmRSA3072": {
"message": "RSA 3072-Bit"
"message": "RSA a 3072 bit"
},
"sshKeyAlgorithmRSA4096": {
"message": "RSA 4096-Bit"
"message": "RSA a 4096 bit"
},
"sshKeyGenerated": {
"message": "A new SSH key was generated"
"message": "È stata generata una nuova chiave SSH"
},
"sshKeyWrongPassword": {
"message": "The password you entered is incorrect."
"message": "La password inserita non è corretta."
},
"importSshKey": {
"message": "Import"
"message": "Importa"
},
"confirmSshKeyPassword": {
"message": "Confirm password"
"message": "Conferma password"
},
"enterSshKeyPasswordDesc": {
"message": "Enter the password for the SSH key."
"message": "Inserisci la password per la chiave SSH."
},
"enterSshKeyPassword": {
"message": "Enter password"
"message": "Inserisci password"
},
"sshAgentUnlockRequired": {
"message": "Please unlock your vault to approve the SSH key request."
"message": "Sbloccare la cassaforte per approvare la richiesta di chiave SSH."
},
"sshAgentUnlockTimeout": {
"message": "SSH key request timed out."
"message": "Richiesta chiave SSH scaduta."
},
"enableSshAgent": {
"message": "Enable SSH agent"
"message": "Abilita agente SSH"
},
"enableSshAgentDesc": {
"message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault."
"message": "Abilita l'agente SSH per firmare le richieste SSH direttamente dalla tua cassaforte Bitwarden."
},
"enableSshAgentHelp": {
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault."
"message": "L'agente SSH è un servizio rivolto agli sviluppatori che consente di firmare le richieste SSH direttamente dalla tua cassaforte Bitwarden."
},
"premiumRequired": {
"message": "Premium necessario"
@@ -461,10 +461,10 @@
"message": "Copia password"
},
"regenerateSshKey": {
"message": "Regenerate SSH key"
"message": "Rigenera la chiave SSH"
},
"copySshPrivateKey": {
"message": "Copy SSH private key"
"message": "Copia chiave privata SSH"
},
"copyPassphrase": {
"message": "Copia passphrase",
@@ -624,7 +624,7 @@
"message": "Crea account"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Nuovo in Bitwarden?"
},
"setAStrongPassword": {
"message": "Imposta una password robusta"
@@ -636,16 +636,16 @@
"message": "Accedi"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "Accedi a Bitwarden"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Accedi con passkey"
},
"loginWithDevice": {
"message": "Log in with device"
"message": "Accedi con dispositivo"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Usa il Single Sign-On"
},
"submit": {
"message": "Invia"
@@ -920,10 +920,10 @@
"message": "URL del server"
},
"authenticationTimeout": {
"message": "Authentication timeout"
"message": "Timeout autenticazione"
},
"authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process."
"message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
},
"selfHostBaseUrl": {
"message": "URL server autogestito",
@@ -1393,13 +1393,13 @@
"message": "Cronologia delle password"
},
"generatorHistory": {
"message": "Generator history"
"message": "Cronologia generatore"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Cancella cronologia generatore"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
},
"clear": {
"message": "Cancella",
@@ -1409,13 +1409,13 @@
"message": "Non ci sono password da mostrare."
},
"clearHistory": {
"message": "Clear history"
"message": "Cancella cronologia"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Niente da mostrare"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "Non hai generato niente di recente"
},
"undo": {
"message": "Annulla"
@@ -1771,10 +1771,10 @@
"message": "L'eliminazione del tuo account è permanente. Non può essere annullata."
},
"cannotDeleteAccount": {
"message": "Cannot delete account"
"message": "Impossibile eliminare account"
},
"cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
"message": "Questa azione non può essere completata perché il tuo account è di proprietà di un'organizzazione. Contatta l'amministratore della tua organizzazione per dettagli."
},
"accountDeleted": {
"message": "Account eliminato"
@@ -2481,7 +2481,7 @@
"message": "Genera e-mail"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@@ -2495,7 +2495,7 @@
}
},
"passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
"message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2505,7 +2505,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
"message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2726,13 +2726,13 @@
"message": "Una notifica è stata inviata al tuo dispositivo."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
"message": "Una notifica è stata inviata al tuo dispositivo"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
},
"needAnotherOptionV1": {
"message": "Need another option?"
"message": "Bisogno di un'altra opzione?"
},
"fingerprintMatchInfo": {
"message": "Assicurati che la tua cassaforte sia sbloccata e che la frase impronta corrisponda sull'altro dispositivo."
@@ -2741,13 +2741,13 @@
"message": "Frase impronta"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
"message": "Sarai notificato una volta che la richiesta sarà approvata"
},
"needAnotherOption": {
"message": "L'accesso con dispositivo deve essere abilitato nelle impostazioni dell'app Bitwarden. Ti serve un'altra opzione?"
},
"viewAllLogInOptions": {
"message": "View all log in options"
"message": "Visualizza tutte le opzioni di accesso"
},
"viewAllLoginOptions": {
"message": "Visualizza tutte le opzioni di accesso"
@@ -2869,7 +2869,7 @@
"message": "Controlla se la tua password è presente in una violazione dei dati"
},
"loggedInExclamation": {
"message": "Logged in!"
"message": "Accesso effettuato!"
},
"important": {
"message": "Importante:"
@@ -2902,16 +2902,16 @@
"message": "Aggiornamento delle impostazioni consigliato"
},
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless"
"message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
},
"deviceApprovalRequired": {
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
},
"deviceApprovalRequiredV2": {
"message": "Device approval required"
"message": "Approvazione dispositivo richiesta"
},
"selectAnApprovalOptionBelow": {
"message": "Select an approval option below"
"message": "Seleziona un'opzione di approvazione sotto"
},
"rememberThisDevice": {
"message": "Ricorda questo dispositivo"
@@ -2966,7 +2966,7 @@
"message": "Email utente mancante"
},
"activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out."
"message": "Email utente attiva non trovata. Logout in corso."
},
"deviceTrusted": {
"message": "Dispositivo fidato"
@@ -3363,55 +3363,55 @@
"message": "Non è stato possibile trovare nessuna porta libera per il login Sso."
},
"authorize": {
"message": "Authorize"
"message": "Autorizza"
},
"deny": {
"message": "Deny"
"message": "Nega"
},
"sshkeyApprovalTitle": {
"message": "Confirm SSH key usage"
"message": "Conferma l'uso della chiave SSH"
},
"sshkeyApprovalMessageInfix": {
"message": "is requesting access to"
"message": "richiede l'accesso a"
},
"unknownApplication": {
"message": "An application"
"message": "Un'applicazione"
},
"sshKeyPasswordUnsupported": {
"message": "Importing password protected SSH keys is not yet supported"
"message": "L'importazione di chiavi SSH protette da password non è ancora supportata"
},
"invalidSshKey": {
"message": "The SSH key is invalid"
"message": "La chiave SSH non è valida"
},
"sshKeyTypeUnsupported": {
"message": "The SSH key type is not supported"
"message": "Il tipo di chiave SSH non è supportato"
},
"importSshKeyFromClipboard": {
"message": "Import key from clipboard"
"message": "Importa chiave dagli Appunti"
},
"sshKeyPasted": {
"message": "SSH key imported successfully"
"message": "Chiave SSH importata correttamente"
},
"fileSavedToDevice": {
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
},
"importantNotice": {
"message": "Important notice"
"message": "Notifica importante"
},
"setupTwoStepLogin": {
"message": "Set up two-step login"
"message": "Imposta accesso in due passaggi"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden invierà un codice all'e-mail del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
},
"remindMeLater": {
"message": "Remind me later"
"message": "Ricordamelo più tardi"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
"message": "Hai accesso affidabile alla tua e-mail, $EMAIL$?",
"placeholders": {
"email": {
"content": "$1",
@@ -3420,15 +3420,15 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not"
"message": "No, non ce l'ho"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "Sì, posso accedere in modo affidabile alla mia e-mail"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "Attiva accesso in due passaggi"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "Cambia l'e-mail dell'account"
}
}

View File

@@ -27,7 +27,7 @@
"message": "セキュアメモ"
},
"typeSshKey": {
"message": "SSH key"
"message": "SSH キー"
},
"folders": {
"message": "フォルダー"
@@ -64,7 +64,7 @@
}
},
"welcomeBack": {
"message": "Welcome back"
"message": "ようこそ"
},
"moveToOrgDesc": {
"message": "このアイテムを移動する組織を選択してください。組織に移動すると、アイテムの所有権がその組織に移行します。 このアイテムが移動された後、あなたはこのアイテムの直接の所有者にはなりません。"
@@ -181,16 +181,16 @@
"message": "住所"
},
"sshPrivateKey": {
"message": "Private key"
"message": "秘密鍵"
},
"sshPublicKey": {
"message": "Public key"
"message": "公開鍵"
},
"sshFingerprint": {
"message": "Fingerprint"
"message": "フィンガープリント"
},
"sshKeyAlgorithm": {
"message": "Key type"
"message": "キーの種類"
},
"sshKeyAlgorithmED25519": {
"message": "ED25519"
@@ -205,37 +205,37 @@
"message": "RSA 4096-Bit"
},
"sshKeyGenerated": {
"message": "A new SSH key was generated"
"message": "新しい SSH 鍵が生成されました"
},
"sshKeyWrongPassword": {
"message": "The password you entered is incorrect."
"message": "入力されたパスワードが間違っています。"
},
"importSshKey": {
"message": "Import"
"message": "インポート"
},
"confirmSshKeyPassword": {
"message": "Confirm password"
"message": "パスワードを確認"
},
"enterSshKeyPasswordDesc": {
"message": "Enter the password for the SSH key."
"message": "SSH キーのパスワードを入力します。"
},
"enterSshKeyPassword": {
"message": "Enter password"
"message": "パスワードを入力"
},
"sshAgentUnlockRequired": {
"message": "Please unlock your vault to approve the SSH key request."
"message": "SSH キーリクエストを承認するには、保管庫のロックを解除してください。"
},
"sshAgentUnlockTimeout": {
"message": "SSH key request timed out."
"message": "SSH キーの要求がタイムアウトしました。"
},
"enableSshAgent": {
"message": "Enable SSH agent"
"message": "SSH エージェントを有効にする"
},
"enableSshAgentDesc": {
"message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault."
"message": "Bitwarden 保管庫から直接 SSH 要求に署名するために SSH エージェントを有効にします。"
},
"enableSshAgentHelp": {
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault."
"message": "SSH エージェントとは、Bitwarden 保管庫から直接 SSH リクエストに署名できる、開発者を対象としたサービスです。"
},
"premiumRequired": {
"message": "プレミアム会員専用"
@@ -323,7 +323,7 @@
"message": "パスワードの自動生成"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "パスフレーズを生成"
},
"type": {
"message": "タイプ"
@@ -461,13 +461,13 @@
"message": "パスワードのコピー"
},
"regenerateSshKey": {
"message": "Regenerate SSH key"
"message": "SSH キーを再生成"
},
"copySshPrivateKey": {
"message": "Copy SSH private key"
"message": "SSH 秘密鍵をコピー"
},
"copyPassphrase": {
"message": "Copy passphrase",
"message": "パスフレーズをコピー",
"description": "Copy passphrase to clipboard"
},
"copyUri": {
@@ -624,7 +624,7 @@
"message": "アカウントの作成"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Bitwarden は初めてですか?"
},
"setAStrongPassword": {
"message": "強力なパスワードを設定する"
@@ -636,16 +636,16 @@
"message": "ログイン"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "Bitwarden にログイン"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "パスキーでログイン"
},
"loginWithDevice": {
"message": "Log in with device"
"message": "デバイスでログイン"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "シングルサインオンを使用する"
},
"submit": {
"message": "送信"
@@ -920,13 +920,13 @@
"message": "サーバー URL"
},
"authenticationTimeout": {
"message": "Authentication timeout"
"message": "認証のタイムアウト"
},
"authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process."
"message": "認証セッションの有効期限が切れました。ログイン操作を最初からやり直してください。"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "自己ホスト型サーバーの URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@@ -1320,7 +1320,7 @@
"description": "Copy credit card number"
},
"copyEmail": {
"message": "Copy email"
"message": "メールアドレスをコピー"
},
"copySecurityCode": {
"message": "セキュリティコードのコピー",
@@ -1393,13 +1393,13 @@
"message": "パスワードの履歴"
},
"generatorHistory": {
"message": "Generator history"
"message": "生成履歴"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "生成履歴を消去"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "続行すると、すべてのエントリは生成履歴から完全に削除されます。続行してもよろしいですか?"
},
"clear": {
"message": "消去する",
@@ -1409,13 +1409,13 @@
"message": "表示するパスワードがありません"
},
"clearHistory": {
"message": "Clear history"
"message": "履歴を消去"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "表示するものがありません"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "最近生成したものはありません"
},
"undo": {
"message": "元に戻す"
@@ -1771,10 +1771,10 @@
"message": "アカウントを恒久的に削除します。元に戻すことはできません。"
},
"cannotDeleteAccount": {
"message": "Cannot delete account"
"message": "アカウントを削除できません"
},
"cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
"message": "このアカウントは組織が所有しているため、操作を完了できません。詳しくは組織の管理者へご確認ください。"
},
"accountDeleted": {
"message": "アカウントが削除されました"
@@ -2478,10 +2478,10 @@
"message": "ユーザー名を生成"
},
"generateEmail": {
"message": "Generate email"
"message": "メールアドレスを生成"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"message": "値は $MIN$ から $MAX$ の間でなければなりません。",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@@ -2495,7 +2495,7 @@
}
},
"passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
"message": " 強力なパスワードを生成するには、 $RECOMMENDED$ 文字以上を使用してください。",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2505,7 +2505,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
"message": " 強力なパスフレーズを生成するには、 $RECOMMENDED$ 単語以上を使用してください。",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2558,11 +2558,11 @@
"message": "外部転送サービスを使用してメールエイリアスを生成します。"
},
"forwarderDomainName": {
"message": "Email domain",
"message": "メールアドレスのドメイン",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "選択したサービスでサポートされているドメインを選択してください",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@@ -2726,13 +2726,13 @@
"message": "デバイスに通知を送信しました。"
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
"message": "お使いのデバイスに通知が送信されました"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "アカウントがロック解除されていることと、フィンガープリントフレーズが他の端末と一致していることを確認してください"
},
"needAnotherOptionV1": {
"message": "Need another option?"
"message": "別の選択肢が必要ですか?"
},
"fingerprintMatchInfo": {
"message": "保管庫がロックされていることと、パスフレーズが他のデバイスと一致していることを確認してください。"
@@ -2741,13 +2741,13 @@
"message": "パスフレーズ"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
"message": "リクエストが承認されると通知されます"
},
"needAnotherOption": {
"message": "Bitwarden アプリの設定でデバイスでログインする必要があります。別のオプションが必要ですか?"
},
"viewAllLogInOptions": {
"message": "View all log in options"
"message": "すべてのログインオプションを表示"
},
"viewAllLoginOptions": {
"message": "すべてのログインオプションを表示"
@@ -2869,7 +2869,7 @@
"message": "このパスワードの既知のデータ流出を確認"
},
"loggedInExclamation": {
"message": "Logged in!"
"message": "ログインしました!"
},
"important": {
"message": "重要"
@@ -2902,16 +2902,16 @@
"message": "設定の更新を推奨"
},
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless"
"message": "このデバイスを記憶して今後のログインをシームレスにする"
},
"deviceApprovalRequired": {
"message": "デバイスの承認が必要です。以下から承認オプションを選択してください:"
},
"deviceApprovalRequiredV2": {
"message": "Device approval required"
"message": "デバイスの承認が必要です"
},
"selectAnApprovalOptionBelow": {
"message": "Select an approval option below"
"message": "以下の承認オプションを選択してください"
},
"rememberThisDevice": {
"message": "このデバイスを記憶する"
@@ -2966,7 +2966,7 @@
"message": "ユーザーのメールアドレスがありません"
},
"activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out."
"message": "アクティブなユーザーメールアドレスが見つかりません。ログアウトします。"
},
"deviceTrusted": {
"message": "信頼されたデバイス"
@@ -3363,55 +3363,55 @@
"message": "SSO ログインのための空きポートが見つかりませんでした。"
},
"authorize": {
"message": "Authorize"
"message": "認可"
},
"deny": {
"message": "Deny"
"message": "拒否"
},
"sshkeyApprovalTitle": {
"message": "Confirm SSH key usage"
"message": "SSH 鍵の使用を確認します"
},
"sshkeyApprovalMessageInfix": {
"message": "is requesting access to"
"message": "がアクセスを要求しています: "
},
"unknownApplication": {
"message": "An application"
"message": "アプリ"
},
"sshKeyPasswordUnsupported": {
"message": "Importing password protected SSH keys is not yet supported"
"message": "パスワードで保護された SSH キーのインポートはまだサポートされていません"
},
"invalidSshKey": {
"message": "The SSH key is invalid"
"message": "SSH キーが無効です"
},
"sshKeyTypeUnsupported": {
"message": "The SSH key type is not supported"
"message": "サポートされていない種類の SSH キーです"
},
"importSshKeyFromClipboard": {
"message": "Import key from clipboard"
"message": "クリップボードからキーをインポート"
},
"sshKeyPasted": {
"message": "SSH key imported successfully"
"message": "SSH キーのインポートに成功しました"
},
"fileSavedToDevice": {
"message": "ファイルをデバイスに保存しました。デバイスのダウンロードで管理できます。"
},
"importantNotice": {
"message": "Important notice"
"message": "重要なお知らせ"
},
"setupTwoStepLogin": {
"message": "Set up two-step login"
"message": "2段階認証によるログインを設定する"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
},
"remindMeLater": {
"message": "Remind me later"
"message": "後で再通知"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
"message": "新しいメールアドレス $EMAIL$ はあなたが管理しているものですか?",
"placeholders": {
"email": {
"content": "$1",
@@ -3420,15 +3420,15 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not"
"message": "いいえ、違います"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "はい、メールアドレスには私が確実にアクセスできます"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "2段階認証によるログインを有効にする"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "アカウントのメールアドレスを変更する"
}
}

View File

@@ -945,7 +945,7 @@
"message": "Pictogrammenserver-URL"
},
"environmentSaved": {
"message": "De omgevings-URL's zijn opgeslagen."
"message": "Omgevings-URL's opgeslagen"
},
"ok": {
"message": "Ok"
@@ -1002,7 +1002,7 @@
"message": "Nieuwe map toevoegen"
},
"view": {
"message": "Beeld"
"message": "Weergeven"
},
"account": {
"message": "Account"
@@ -1268,7 +1268,7 @@
"description": "Copy to clipboard"
},
"checkForUpdates": {
"message": "Controleren op updates"
"message": "Controleren op updates"
},
"version": {
"message": "Versie $VERSION_NUM$",
@@ -3026,7 +3026,7 @@
}
},
"multipleInputEmails": {
"message": "Een of meer e-mailadressen zijn ongeldig"
"message": "Eén of meer e-mailadressen zijn ongeldig"
},
"inputTrimValidator": {
"message": "Invoer mag niet alleen witruimte bevatten.",
@@ -3051,7 +3051,7 @@
"message": "-- Type om te filteren --"
},
"multiSelectLoading": {
"message": "Opties ophalen..."
"message": "Opties ophalen"
},
"multiSelectNotFound": {
"message": "Geen items gevonden"
@@ -3243,7 +3243,7 @@
"message": "LastPass Email"
},
"importingYourAccount": {
"message": "Account impoteren..."
"message": "Account impoteren"
},
"lastPassMFARequired": {
"message": "LastPass multifactor-authenticatie vereist"
@@ -3396,7 +3396,7 @@
"message": "Bestand op apparaat opgeslagen. Beheer vanaf de downloads op je apparaat."
},
"importantNotice": {
"message": "Belangrijke mededeling"
"message": "Belangrijke melding"
},
"setupTwoStepLogin": {
"message": "Tweestapsaanmelding instellen"

View File

@@ -3399,7 +3399,7 @@
"message": "Dôležité upozornenie"
},
"setupTwoStepLogin": {
"message": "Nastavenie dvojstupňového prihlásenia"
"message": "Nastav dvojstupňové prihlásenie"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden vám od februára 2025 pošle na e-mail vášho účtu kód na overenie prihlásenia z nových zariadení."

View File

@@ -208,19 +208,19 @@
"message": "Генерисан је нови SSH кључ"
},
"sshKeyWrongPassword": {
"message": "The password you entered is incorrect."
"message": "Лозинка коју сте унели није тачна."
},
"importSshKey": {
"message": "Import"
"message": "Увоз"
},
"confirmSshKeyPassword": {
"message": "Confirm password"
"message": "Потврда лозинке"
},
"enterSshKeyPasswordDesc": {
"message": "Enter the password for the SSH key."
"message": "Унети лозинку за SSH кључ."
},
"enterSshKeyPassword": {
"message": "Enter password"
"message": "Унесите лозинку"
},
"sshAgentUnlockRequired": {
"message": "Откључајте свој сеф да бисте одобрили захтев за SSH кључ."
@@ -920,10 +920,10 @@
"message": "УРЛ Сервера"
},
"authenticationTimeout": {
"message": "Authentication timeout"
"message": "Истекло је време аутентификације"
},
"authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process."
"message": "Истекло је време сесије за аутентификацију. Молим вас покрените процес пријаве поново."
},
"selfHostBaseUrl": {
"message": "УРЛ сервера који се самостално хостује",
@@ -1393,13 +1393,13 @@
"message": "Историја Лозинке"
},
"generatorHistory": {
"message": "Generator history"
"message": "Генератор историје"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Испразнити генератор историје"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Ако наставите, сви уноси ће бити трајно избрисани из генератора историје. Да ли сте сигурни да желите да наставите?"
},
"clear": {
"message": "Очисти",
@@ -1409,13 +1409,13 @@
"message": "Нема лозинки за приказивање."
},
"clearHistory": {
"message": "Clear history"
"message": "Обриши историју"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Ништа за приказ"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "Недавно нисте ништа генерисали"
},
"undo": {
"message": "Опозови"
@@ -2481,7 +2481,7 @@
"message": "Генеришите имејл"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"message": "Вредност мора бити између $MIN$ и $MAX$.",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@@ -2495,7 +2495,7 @@
}
},
"passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
"message": " Употребити $RECOMMENDED$ знакова или више да бисте генерисали јаку лозинку.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2505,7 +2505,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
"message": " Употребити $RECOMMENDED$ речи или више да бисте генерисали јаку приступну фразу.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2726,13 +2726,13 @@
"message": "Обавештење је послато на ваш уређај."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
"message": "Обавештење је послато на ваш уређај"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају"
},
"needAnotherOptionV1": {
"message": "Need another option?"
"message": "Треба Вам друга опција?"
},
"fingerprintMatchInfo": {
"message": "Уверите се да је ваш сеф откључан и да се фраза отиска прста подудара на другом уређају."
@@ -2741,13 +2741,13 @@
"message": "Сигурносна фраза сефа"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
"message": "Бићете обавештени када захтев буде одобрен"
},
"needAnotherOption": {
"message": "Пријава помоћу уређаја мора бити подешена у подешавањима Bitwarden апликације. Потребна је друга опција?"
},
"viewAllLogInOptions": {
"message": "View all log in options"
"message": "Погледајте сав извештај у опције"
},
"viewAllLoginOptions": {
"message": "Погредајте све опције пријављивања"
@@ -2869,7 +2869,7 @@
"message": "Проверите познате упада података за ову лозинку"
},
"loggedInExclamation": {
"message": "Logged in!"
"message": "Пријављено!"
},
"important": {
"message": "Важно:"
@@ -2902,16 +2902,16 @@
"message": "Препоручено ажурирање поставки"
},
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless"
"message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне"
},
"deviceApprovalRequired": {
"message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:"
},
"deviceApprovalRequiredV2": {
"message": "Device approval required"
"message": "Потребно је одобрење уређаја"
},
"selectAnApprovalOptionBelow": {
"message": "Select an approval option below"
"message": "Изаберите опцију одобрења у наставку"
},
"rememberThisDevice": {
"message": "Запамти овај уређај"
@@ -2966,7 +2966,7 @@
"message": "Недостаје имејл корисника"
},
"activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out."
"message": "Имејл активног корисника није пронађен. Одјављивање."
},
"deviceTrusted": {
"message": "Уређај поуздан"

View File

@@ -3363,10 +3363,10 @@
"message": "SSO girişi için açık port bulunamadı."
},
"authorize": {
"message": "Authorize"
"message": "Yetkilendir"
},
"deny": {
"message": "Deny"
"message": "Reddet"
},
"sshkeyApprovalTitle": {
"message": "Confirm SSH key usage"

View File

@@ -742,7 +742,7 @@
"message": "必须填写确认主密码。"
},
"masterPasswordMinlength": {
"message": "主密码必须至少 $VALUE$ 个字符长度。",
"message": "主密码长度必须至少 $VALUE$ 个字符。",
"description": "The Master Password must be at least a specific number of characters long.",
"placeholders": {
"value": {
@@ -1044,7 +1044,7 @@
"message": "前往网页 App 吗?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。"
"message": "您可以在 Bitwarden 网页 App 上更改您的主密码。"
},
"fingerprintPhrase": {
"message": "指纹短语",
@@ -1058,7 +1058,7 @@
"message": "转到网页版密码库"
},
"getMobileApp": {
"message": "获取移动应用程序"
"message": "获取移动 App"
},
"getBrowserExtension": {
"message": "获取浏览器扩展"
@@ -1247,13 +1247,13 @@
"message": "语言"
},
"languageDesc": {
"message": "更改应用程序所使用的语言。重新启动后生效。"
"message": "更改应用程序所使用的语言。重后生效。"
},
"theme": {
"message": "主题"
},
"themeDesc": {
"message": "更改应用程序的颜色主题。"
"message": "更改应用程序的颜色主题。"
},
"dark": {
"message": "深色",
@@ -1665,7 +1665,7 @@
"message": "确认密码库导出"
},
"exportWarningDesc": {
"message": "导出的密码库数据包含未加密格式。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。用完后请立即将其删除。"
"message": "导出包含未加密格式的密码库数据。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。使用完后请立即将其删除。"
},
"encExportKeyWarningDesc": {
"message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。"
@@ -1711,7 +1711,7 @@
"message": "使用 PIN 码解锁"
},
"setYourPinCode": {
"message": "设定您用来解锁 Bitwarden 的 PIN 码。您的 PIN 设置将在您完全注销应用程序时被重置。"
"message": "设置用于解锁 Bitwarden 的 PIN 码。您的 PIN 设置将在您完全注销应用程序时被重置。"
},
"pinRequired": {
"message": "需要 PIN 码。"
@@ -1753,7 +1753,7 @@
"message": "应用程序启动时要求使用触控 ID"
},
"requirePasswordOnStart": {
"message": "应用程序启动时要求输入密码或 PIN 码"
"message": "App 启动时要求输入密码或 PIN 码"
},
"recommendedForSecurity": {
"message": "安全起见,推荐设置。"
@@ -2402,7 +2402,7 @@
"message": "偏好设置"
},
"appPreferences": {
"message": "应用设置(所有账户)"
"message": "应用程序设置(所有账户)"
},
"accountSwitcherLimitReached": {
"message": "已达到账户上限。请注销一个账户后再添加其他账户。"
@@ -3369,7 +3369,7 @@
"message": "拒绝"
},
"sshkeyApprovalTitle": {
"message": "确认 SSH 密钥的使用方式"
"message": "确认 SSH 密钥的使用"
},
"sshkeyApprovalMessageInfix": {
"message": "正在请求访问"
@@ -3402,7 +3402,7 @@
"message": "设置两步登录"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "从 2025 年 02 月开始Bitwarden 将向您的账户电子邮箱发送一个代码,以验证来自新设备的登录。"
"message": "从 2025 年 02 月起,当有来自新设备的登录时Bitwarden 将向您的账户电子邮箱发送验证码。"
},
"newDeviceVerificationNoticeContentPage2": {
"message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。"
@@ -3411,7 +3411,7 @@
"message": "稍后提醒我"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "您能可靠地访问您的电子邮箱 $EMAIL$ 吗?",
"message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?",
"placeholders": {
"email": {
"content": "$1",
@@ -3423,7 +3423,7 @@
"message": "不,我不能"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "是的,我可以可靠地访问我的电子邮箱"
"message": "是的,我可以正常访问我的电子邮箱"
},
"turnOnTwoStepLogin": {
"message": "开启两步登录"

View File

@@ -23,50 +23,6 @@
"node_modules/@bitwarden/desktop-napi": {
"resolved": "../desktop_native/napi",
"link": true
},
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/argon2": {
"version": "0.41.1",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz",
"integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@phc/format": "^1.0.0",
"node-addon-api": "^8.1.0",
"node-gyp-build": "^4.8.1"
},
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/node-addon-api": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz",
"integrity": "sha512-vmEOvxwiH8tlOcv4SyE8RH34rI5/nWVaigUeAUPawC6f0+HoDthwI0vkMu4tbtsZrXq6QXFfrkhjofzKEs5tpA==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz",
"integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
}
}
}

View File

@@ -208,6 +208,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
});
} else {
password = await this.getSshKeyPassword();
if (password === "") {
return;
}
await this.importSshKeyFromClipboard(password);
}
return;

View File

@@ -131,7 +131,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
protected getUserName(r: EventResponse, userId: string) {
if (r.installationId != null) {
return `Installation: ${r.installationId}`;
return {
name: `Installation: ${r.installationId}`,
};
}
if (userId != null) {

View File

@@ -6,7 +6,11 @@ import { mock } from "jest-mock-extended";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -29,6 +33,8 @@ describe("EmergencyViewDialogComponent", () => {
card: {},
} as CipherView;
const accountService: FakeAccountService = mockAccountServiceWith(Utils.newGuid() as UserId);
beforeEach(async () => {
open.mockClear();
close.mockClear();
@@ -43,6 +49,7 @@ describe("EmergencyViewDialogComponent", () => {
{ provide: DialogService, useValue: { open } },
{ provide: DialogRef, useValue: { close } },
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
{ provide: AccountService, useValue: accountService },
],
}).compileComponents();

View File

@@ -58,7 +58,7 @@
*ngIf="deprecateStripeSourcesAPI"
[showAccountCredit]="false"
></app-payment-v2>
<app-tax-info [trialFlow]="true" (onCountryChanged)="changedCountry()"></app-tax-info>
<app-tax-info [trialFlow]="true" (countryChanged)="changedCountry()"></app-tax-info>
</div>
<div class="tw-flex tw-space-x-2">
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">

View File

@@ -131,11 +131,10 @@
<bit-section>
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
<app-payment-v2 [showBankAccount]="false"></app-payment-v2>
<app-tax-info></app-tax-info>
<app-tax-info (taxInformationChanged)="onTaxInformationChanged()"></app-tax-info>
<div class="tw-mb-4">
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
<!-- TODO: Currently incorrect - https://bitwarden.atlassian.net/browse/PM-11525 -->
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
</div>
</div>

View File

@@ -5,10 +5,13 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, concatMap, from, Observable, of } from "rxjs";
import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -44,6 +47,7 @@ export class PremiumV2Component {
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
);
protected estimatedTax: number = 0;
protected readonly familyPlanMaxUserCount = 6;
protected readonly premiumPrice = 10;
protected readonly storageGBPrice = 4;
@@ -60,6 +64,7 @@ export class PremiumV2Component {
private syncService: SyncService,
private toastService: ToastService,
private tokenService: TokenService,
private taxService: TaxServiceAbstraction,
) {
this.isSelfHost = this.platformUtilsService.isSelfHost();
@@ -82,6 +87,12 @@ export class PremiumV2Component {
}),
)
.subscribe();
this.addOnFormGroup.controls.additionalStorage.valueChanges
.pipe(debounceTime(1000), takeUntilDestroyed())
.subscribe(() => {
this.refreshSalesTax();
});
}
finalizeUpgrade = async () => {
@@ -158,12 +169,6 @@ export class PremiumV2Component {
return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
}
protected get estimatedTax(): number {
return this.taxInfoComponent?.taxRate != null
? (this.taxInfoComponent.taxRate / 100) * this.subtotal
: 0;
}
protected get premiumURL(): string {
return `${this.cloudWebVaultURL}/#/settings/subscription/premium`;
}
@@ -179,4 +184,36 @@ export class PremiumV2Component {
protected async onLicenseFileSelectedChanged(): Promise<void> {
await this.postFinalizeUpgrade();
}
private refreshSalesTax(): void {
if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) {
return;
}
const request: PreviewIndividualInvoiceRequest = {
passwordManager: {
additionalStorage: this.addOnFormGroup.value.additionalStorage,
},
taxInformation: {
postalCode: this.taxInfoComponent.postalCode,
country: this.taxInfoComponent.country,
},
};
this.taxService
.previewIndividualInvoice(request)
.then((invoice) => {
this.estimatedTax = invoice.taxAmount;
})
.catch((error) => {
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
});
});
}
protected onTaxInformationChanged(): void {
this.refreshSalesTax();
}
}

View File

@@ -122,7 +122,7 @@
<bit-section>
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
<app-payment [hideBank]="true"></app-payment>
<app-tax-info></app-tax-info>
<app-tax-info (taxInformationChanged)="onTaxInformationChanged()" />
<div id="price" class="tw-my-4">
<div class="tw-text-muted tw-text-sm">
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}

View File

@@ -1,13 +1,17 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs";
import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -39,6 +43,9 @@ export class PremiumComponent implements OnInit {
protected addonForm = new FormGroup({
additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]),
});
private estimatedTax: number = 0;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
@@ -50,9 +57,16 @@ export class PremiumComponent implements OnInit {
private environmentService: EnvironmentService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
private taxService: TaxServiceAbstraction,
) {
this.selfHosted = platformUtilsService.isSelfHost();
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
this.addonForm.controls.additionalStorage.valueChanges
.pipe(debounceTime(1000), takeUntilDestroyed())
.subscribe(() => {
this.refreshSalesTax();
});
}
protected setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
@@ -154,12 +168,42 @@ export class PremiumComponent implements OnInit {
}
get taxCharges(): number {
return this.taxInfoComponent != null && this.taxInfoComponent.taxRate != null
? (this.taxInfoComponent.taxRate / 100) * this.subtotal
: 0;
return this.estimatedTax;
}
get total(): number {
return this.subtotal + this.taxCharges || 0;
}
private refreshSalesTax(): void {
if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) {
return;
}
const request: PreviewIndividualInvoiceRequest = {
passwordManager: {
additionalStorage: this.addonForm.value.additionalStorage,
},
taxInformation: {
postalCode: this.taxInfoComponent.postalCode,
country: this.taxInfoComponent.country,
},
};
this.taxService
.previewIndividualInvoice(request)
.then((invoice) => {
this.estimatedTax = invoice.taxAmount;
})
.catch((error) => {
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
});
});
}
protected onTaxInformationChanged(): void {
this.refreshSalesTax();
}
}

View File

@@ -5,6 +5,8 @@ import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ToastService } from "@bitwarden/components";
@@ -34,6 +36,7 @@ export class AdjustSubscription implements OnInit, OnDestroy {
private organizationApiService: OrganizationApiServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
private internalOrganizationService: InternalOrganizationServiceAbstraction,
) {}
ngOnInit() {
@@ -64,7 +67,20 @@ export class AdjustSubscription implements OnInit, OnDestroy {
this.additionalSeatCount,
this.adjustSubscriptionForm.value.newMaxSeats,
);
await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request);
const response = await this.organizationApiService.updatePasswordManagerSeats(
this.organizationId,
request,
);
const organization = await this.internalOrganizationService.get(this.organizationId);
const organizationData = new OrganizationData(response, {
isMember: organization.isMember,
isProviderUser: organization.isProviderUser,
});
await this.internalOrganizationService.upsert(organizationData);
this.toastService.showToast({
variant: "success",

View File

@@ -344,25 +344,14 @@
</span>
<a></a>
</p>
<app-payment
*ngIf="
(upgradeRequiresPaymentMethod || showPayment || isPaymentSourceEmpty()) &&
!deprecateStripeSourcesAPI
"
[hideCredit]="true"
></app-payment>
<app-payment-v2
*ngIf="
(upgradeRequiresPaymentMethod || showPayment || isPaymentSourceEmpty()) &&
deprecateStripeSourcesAPI
"
[showAccountCredit]="false"
>
</app-payment-v2>
<app-tax-info
*ngIf="showPayment || upgradeRequiresPaymentMethod || isPaymentSourceEmpty()"
(onCountryChanged)="changedCountry()"
></app-tax-info>
<ng-container *ngIf="canUpdatePaymentInformation()">
<app-payment *ngIf="!deprecateStripeSourcesAPI" [hideCredit]="true" />
<app-payment-v2 *ngIf="deprecateStripeSourcesAPI" [showAccountCredit]="false" />
<app-manage-tax-information
[startWith]="taxInformation"
(taxInformationChanged)="taxInformationChanged($event)"
></app-manage-tax-information>
</ng-container>
<div id="price" class="tw-mt-4">
<p class="tw-text-lg tw-mb-1">
<span class="tw-font-semibold"
@@ -947,6 +936,20 @@
</p>
</bit-hint>
</div>
<div *ngIf="totalOpened" class="row tw-mt-4">
<bit-hint class="col-6">
<p
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
>
<span class="tw-font-semibold">
{{ "estimatedTax" | i18n }}
</span>
<span>
{{ estimatedTax | currency: "USD" : "$" }}
</span>
</p>
</bit-hint>
</div>
<div *ngIf="totalOpened" id="price" class="row tw-mt-4">
<bit-hint class="col-6">
<p

View File

@@ -15,6 +15,7 @@ import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -24,13 +25,17 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import {
PaymentMethodType,
PlanInterval,
PlanType,
ProductTierType,
} from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
@@ -46,7 +51,6 @@ import { KeyService } from "@bitwarden/key-management";
import { PaymentV2Component } from "../shared/payment/payment-v2.component";
import { PaymentComponent } from "../shared/payment/payment.component";
import { TaxInfoComponent } from "../shared/tax-info.component";
type ChangePlanDialogParams = {
organizationId: string;
@@ -89,13 +93,12 @@ interface OnSuccessArgs {
export class ChangePlanDialogComponent implements OnInit, OnDestroy {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent;
@Input() acceptingSponsorship = false;
@Input() organizationId: string;
@Input() showFree = false;
@Input() showCancel = false;
selectedFile: File;
@Input()
get productTier(): ProductTierType {
@@ -107,6 +110,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.formGroup?.controls?.productTier?.setValue(product);
}
protected estimatedTax: number = 0;
private _productTier = ProductTierType.Free;
@Input()
@@ -173,6 +177,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected taxInformation: TaxInformation;
constructor(
@Inject(DIALOG_DATA) private dialogParams: ChangePlanDialogParams,
private dialogRef: DialogRef<ChangePlanDialogResultType>,
@@ -189,6 +195,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
private organizationApiService: OrganizationApiServiceAbstraction,
private configService: ConfigService,
private billingApiService: BillingApiServiceAbstraction,
private taxService: TaxServiceAbstraction,
) {}
async ngOnInit(): Promise<void> {
@@ -267,6 +274,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.setInitialPlanSelection();
this.loading = false;
const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId);
this.taxInformation = TaxInformation.from(taxInfo);
this.refreshSalesTax();
}
setInitialPlanSelection() {
@@ -402,6 +414,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
this.selectedPlan = plan;
this.formGroup.patchValue({ productTier: plan.productTier });
try {
this.refreshSalesTax();
} catch {
this.estimatedTax = 0;
}
}
ngOnDestroy() {
@@ -567,12 +585,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
);
}
get taxCharges() {
return this.taxComponent != null && this.taxComponent.taxRate != null
? (this.taxComponent.taxRate / 100) * this.passwordManagerSubtotal
: 0;
}
get passwordManagerSeats() {
if (this.selectedPlan.productTier === ProductTierType.Families) {
return this.selectedPlan.PasswordManager.baseSeats;
@@ -584,15 +596,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
if (this.organization.useSecretsManager) {
return (
this.passwordManagerSubtotal +
this.additionalStorageTotal(this.selectedPlan) +
this.secretsManagerSubtotal +
this.taxCharges || 0
this.additionalStorageTotal(this.selectedPlan) +
this.secretsManagerSubtotal +
this.estimatedTax
);
}
return (
this.passwordManagerSubtotal +
this.additionalStorageTotal(this.selectedPlan) +
this.taxCharges || 0
this.additionalStorageTotal(this.selectedPlan) +
this.estimatedTax
);
}
@@ -645,8 +657,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
changedCountry() {
if (this.deprecateStripeSourcesAPI && this.paymentV2Component && this.taxComponent) {
this.paymentV2Component.showBankAccount = this.taxComponent.country === "US";
if (this.deprecateStripeSourcesAPI && this.paymentV2Component) {
this.paymentV2Component.showBankAccount = this.taxInformation.country === "US";
if (
!this.paymentV2Component.showBankAccount &&
@@ -654,8 +666,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
) {
this.paymentV2Component.select(PaymentMethodType.Card);
}
} else if (this.paymentComponent && this.taxComponent) {
this.paymentComponent!.hideBank = this.taxComponent?.taxFormGroup?.value.country !== "US";
} else if (this.paymentComponent && this.taxInformation) {
this.paymentComponent!.hideBank = this.taxInformation.country !== "US";
// Bank Account payments are only available for US customers
if (
this.paymentComponent.hideBank &&
@@ -667,9 +679,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
}
protected taxInformationChanged(event: TaxInformation): void {
this.taxInformation = event;
this.changedCountry();
this.refreshSalesTax();
}
submit = async () => {
if (!this.taxComponent?.taxFormGroup.valid && this.taxComponent?.taxFormGroup.touched) {
this.taxComponent?.taxFormGroup.markAllAsTouched();
if (this.taxComponent !== undefined && !this.taxComponent.validate()) {
return;
}
@@ -723,8 +740,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
if (this.showPayment) {
request.billingAddressCountry = this.taxComponent.taxFormGroup?.value.country;
request.billingAddressPostalCode = this.taxComponent.taxFormGroup?.value.postalCode;
request.billingAddressCountry = this.taxInformation.country;
request.billingAddressPostalCode = this.taxInformation.postalCode;
}
// Secrets Manager
@@ -735,15 +752,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
const tokenizedPaymentSource = await this.paymentV2Component.tokenize();
const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource;
updatePaymentMethodRequest.taxInformation = {
country: this.taxComponent.country,
postalCode: this.taxComponent.postalCode,
taxId: this.taxComponent.taxId,
line1: this.taxComponent.line1,
line2: this.taxComponent.line2,
city: this.taxComponent.city,
state: this.taxComponent.state,
};
updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
this.taxInformation,
);
await this.billingApiService.updateOrganizationPaymentMethod(
this.organizationId,
@@ -754,8 +765,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
const paymentRequest = new PaymentRequest();
paymentRequest.paymentToken = tokenResult[0];
paymentRequest.paymentMethodType = tokenResult[1];
paymentRequest.country = this.taxComponent.taxFormGroup?.value.country;
paymentRequest.postalCode = this.taxComponent.taxFormGroup?.value.postalCode;
paymentRequest.country = this.taxInformation.country;
paymentRequest.postalCode = this.taxInformation.postalCode;
await this.organizationApiService.updatePayment(this.organizationId, paymentRequest);
}
}
@@ -944,4 +955,48 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
manageSelectableProduct(index: number) {
return index;
}
private refreshSalesTax(): void {
if (!this.taxInformation.country || !this.taxInformation.postalCode) {
return;
}
const request: PreviewOrganizationInvoiceRequest = {
organizationId: this.organizationId,
passwordManager: {
additionalStorage: 0,
plan: this.selectedPlan?.type,
seats: this.sub.seats,
},
taxInformation: {
postalCode: this.taxInformation.postalCode,
country: this.taxInformation.country,
taxId: this.taxInformation.taxId,
},
};
if (this.organization.useSecretsManager) {
request.secretsManager = {
seats: this.sub.smSeats,
additionalMachineAccounts: this.sub.smServiceAccounts,
};
}
this.taxService
.previewOrganizationInvoice(request)
.then((invoice) => {
this.estimatedTax = invoice.taxAmount;
})
.catch((error) => {
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
});
});
}
protected canUpdatePaymentInformation(): boolean {
return this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty();
}
}

View File

@@ -335,7 +335,7 @@
>{{ "additionalUsers" | i18n }}:</span
>
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ formGroup.controls.additionalSeats.value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12
@@ -355,7 +355,7 @@
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
>
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ formGroup.controls.additionalStorage.value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
@@ -388,7 +388,7 @@
>{{ "additionalUsers" | i18n }}:</span
>
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ formGroup.controls.additionalSeats.value || 0 }} &times;
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{
@@ -403,7 +403,7 @@
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
>
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ formGroup.controls.additionalStorage.value || 0 }} &times;
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
@@ -440,7 +440,12 @@
<app-payment-v2
*ngIf="deprecateStripeSourcesAPI && (createOrganization || upgradeRequiresPaymentMethod)"
></app-payment-v2>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<app-manage-tax-information
class="tw-my-4"
[showTaxIdField]="showTaxIdField"
[startWith]="taxInformation"
(taxInformationChanged)="onTaxInformationChanged($event)"
/>
<div id="price" class="tw-my-4">
<div class="tw-text-muted tw-text-base">
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
@@ -450,7 +455,7 @@
<br />
</span>
<ng-container>
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
</ng-container>
</div>
<hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />

View File

@@ -12,7 +12,9 @@ import {
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { debounceTime } from "rxjs/operators";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -26,9 +28,12 @@ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/mode
import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request";
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
@@ -50,7 +55,6 @@ import { OrganizationCreateModule } from "../../admin-console/organizations/crea
import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared";
import { PaymentV2Component } from "../shared/payment/payment-v2.component";
import { PaymentComponent } from "../shared/payment/payment.component";
import { TaxInfoComponent } from "../shared/tax-info.component";
interface OnSuccessArgs {
organizationId: string;
@@ -72,13 +76,14 @@ const Allowed2020PlansForLegacyProviders = [
export class OrganizationPlansComponent implements OnInit, OnDestroy {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent;
@Input() organizationId: string;
@Input() organizationId?: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
@Input() currentPlan: PlanResponse;
selectedFile: File;
@Input()
@@ -93,6 +98,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private _productTier = ProductTierType.Free;
protected taxInformation: TaxInformation;
@Input()
get plan(): PlanType {
return this._plan;
@@ -149,7 +156,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
billing: BillingResponse;
provider: ProviderResponse;
private destroy$ = new Subject<void>();
protected estimatedTax: number = 0;
protected total: number = 0;
private destroy$: Subject<void> = new Subject<void>();
constructor(
private apiService: ApiService,
@@ -168,6 +178,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private toastService: ToastService,
private configService: ConfigService,
private billingApiService: BillingApiServiceAbstraction,
private taxService: TaxServiceAbstraction,
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
@@ -181,6 +192,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.organization = await this.organizationService.get(this.organizationId);
this.billing = await this.organizationApiService.getBilling(this.organizationId);
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId);
} else {
this.taxInformation = await this.apiService.getTaxInfo();
}
if (!this.selfHosted) {
@@ -241,6 +255,16 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
this.loading = false;
this.formGroup.valueChanges.pipe(debounceTime(1000), takeUntil(this.destroy$)).subscribe(() => {
this.refreshSalesTax();
});
this.secretsManagerForm.valueChanges
.pipe(debounceTime(1000), takeUntil(this.destroy$))
.subscribe(() => {
this.refreshSalesTax();
});
}
ngOnDestroy() {
@@ -438,17 +462,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
return this.selectedPlan.trialPeriodDays != null;
}
get taxCharges() {
return this.taxComponent != null && this.taxComponent.taxRate != null
? (this.taxComponent.taxRate / 100) *
(this.passwordManagerSubtotal + this.secretsManagerSubtotal)
: 0;
}
get total() {
return this.passwordManagerSubtotal + this.secretsManagerSubtotal + this.taxCharges || 0;
}
get paymentDesc() {
if (this.acceptingSponsorship) {
return this.i18nService.t("paymentSponsored");
@@ -554,9 +567,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.changedProduct();
}
changedCountry() {
protected changedCountry(): void {
if (this.deprecateStripeSourcesAPI) {
this.paymentV2Component.showBankAccount = this.taxComponent.country === "US";
this.paymentV2Component.showBankAccount = this.taxInformation?.country === "US";
if (
!this.paymentV2Component.showBankAccount &&
this.paymentV2Component.selected === PaymentMethodType.BankAccount
@@ -564,7 +577,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.paymentV2Component.select(PaymentMethodType.Card);
}
} else {
this.paymentComponent.hideBank = this.taxComponent.taxFormGroup?.value.country !== "US";
this.paymentComponent.hideBank = this.taxInformation?.country !== "US";
if (
this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount
@@ -575,28 +588,31 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
}
cancel() {
protected onTaxInformationChanged(event: TaxInformation): void {
this.taxInformation = event;
this.changedCountry();
this.refreshSalesTax();
}
protected cancel(): void {
this.onCanceled.emit();
}
setSelectedFile(event: Event) {
protected setSelectedFile(event: Event): void {
const fileInputEl = <HTMLInputElement>event.target;
this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
}
submit = async () => {
if (this.taxComponent) {
if (!this.taxComponent?.taxFormGroup.valid) {
this.taxComponent?.taxFormGroup.markAllAsTouched();
return;
}
if (this.taxComponent && !this.taxComponent.validate()) {
return;
}
if (this.singleOrgPolicyBlock) {
return;
}
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
let orgId: string;
if (this.createOrganization) {
const orgKey = await this.keyService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
@@ -607,11 +623,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
const collectionCt = collection.encryptedString;
const orgKeys = await this.keyService.makeKeyPair(orgKey[1]);
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
}
orgId = this.selfHosted
? await this.createSelfHosted(key, collectionCt, orgKeys)
: await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
this.toastService.showToast({
variant: "success",
@@ -619,7 +633,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
message: this.i18nService.t("organizationReadyToGo"),
});
} else {
orgId = await this.updateOrganization(orgId);
orgId = await this.updateOrganization();
this.toastService.showToast({
variant: "success",
title: null,
@@ -653,7 +667,63 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.messagingService.send("organizationCreated", { organizationId });
};
private async updateOrganization(orgId: string) {
protected get showTaxIdField(): boolean {
switch (this.formGroup.controls.productTier.value) {
case ProductTierType.Free:
case ProductTierType.Families:
return false;
default:
return true;
}
}
private refreshSalesTax(): void {
if (this.formGroup.controls.plan.value == PlanType.Free) {
this.estimatedTax = 0;
return;
}
if (!this.taxComponent.validate()) {
return;
}
const request: PreviewOrganizationInvoiceRequest = {
organizationId: this.organizationId,
passwordManager: {
additionalStorage: this.formGroup.controls.additionalStorage.value,
plan: this.formGroup.controls.plan.value,
seats: this.formGroup.controls.additionalSeats.value,
},
taxInformation: {
postalCode: this.taxInformation.postalCode,
country: this.taxInformation.country,
taxId: this.taxInformation.taxId,
},
};
if (this.secretsManagerForm.controls.enabled.value === true) {
request.secretsManager = {
seats: this.secretsManagerForm.controls.userSeats.value,
additionalMachineAccounts: this.secretsManagerForm.controls.additionalServiceAccounts.value,
};
}
this.taxService
.previewOrganizationInvoice(request)
.then((invoice) => {
this.estimatedTax = invoice.taxAmount;
this.total = invoice.totalAmount;
})
.catch((error) => {
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
});
});
}
private async updateOrganization() {
const request = new OrganizationUpgradeRequest();
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
@@ -661,8 +731,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.selectedPlan.PasswordManager.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
request.billingAddressCountry = this.taxComponent.taxFormGroup?.value.country;
request.billingAddressPostalCode = this.taxComponent.taxFormGroup?.value.postalCode;
request.billingAddressCountry = this.taxInformation?.country;
request.billingAddressPostalCode = this.taxInformation?.postalCode;
// Secrets Manager
this.buildSecretsManagerRequest(request);
@@ -671,10 +741,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
if (this.deprecateStripeSourcesAPI) {
const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
updatePaymentMethodRequest.paymentSource = await this.paymentV2Component.tokenize();
const expandedTaxInfoUpdateRequest = new ExpandedTaxInfoUpdateRequest();
expandedTaxInfoUpdateRequest.country = this.taxComponent.country;
expandedTaxInfoUpdateRequest.postalCode = this.taxComponent.postalCode;
updatePaymentMethodRequest.taxInformation = expandedTaxInfoUpdateRequest;
updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
this.taxInformation,
);
await this.billingApiService.updateOrganizationPaymentMethod(
this.organizationId,
updatePaymentMethodRequest,
@@ -684,8 +753,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
const paymentRequest = new PaymentRequest();
paymentRequest.paymentToken = paymentToken;
paymentRequest.paymentMethodType = paymentMethodType;
paymentRequest.country = this.taxComponent.taxFormGroup?.value.country;
paymentRequest.postalCode = this.taxComponent.taxFormGroup?.value.postalCode;
paymentRequest.country = this.taxInformation?.country;
paymentRequest.postalCode = this.taxInformation?.postalCode;
await this.organizationApiService.updatePayment(this.organizationId, paymentRequest);
}
}
@@ -709,7 +778,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
collectionCt: string,
orgKeys: [string, EncString],
orgKey: SymmetricCryptoKey,
) {
): Promise<string> {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;
@@ -738,15 +807,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.selectedPlan.PasswordManager.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxFormGroup?.value.postalCode;
request.billingAddressCountry = this.taxComponent.taxFormGroup?.value.country;
if (this.taxComponent.taxFormGroup?.value.includeTaxId) {
request.taxIdNumber = this.taxComponent.taxFormGroup?.value.taxId;
request.billingAddressLine1 = this.taxComponent.taxFormGroup?.value.line1;
request.billingAddressLine2 = this.taxComponent.taxFormGroup?.value.line2;
request.billingAddressCity = this.taxComponent.taxFormGroup?.value.city;
request.billingAddressState = this.taxComponent.taxFormGroup?.value.state;
}
request.billingAddressPostalCode = this.taxInformation?.postalCode;
request.billingAddressCountry = this.taxInformation?.country;
request.taxIdNumber = this.taxInformation?.taxId;
request.billingAddressLine1 = this.taxInformation?.line1;
request.billingAddressLine2 = this.taxInformation?.line2;
request.billingAddressCity = this.taxInformation?.city;
request.billingAddressState = this.taxInformation?.state;
}
// Secrets Manager

View File

@@ -63,20 +63,5 @@
{{ "paymentChargedWithUnpaidSubscription" | i18n }}
</p>
</bit-section>
<!-- Tax Information -->
<bit-section>
<h2 bitTypography="h2">{{ "taxInformation" | i18n }}</h2>
<p bitTypography="body1">{{ "taxInformationDesc" | i18n }}</p>
<app-tax-info></app-tax-info>
<button
type="button"
bitButton
bitFormButton
buttonType="primary"
[bitAction]="updateTaxInformation"
>
{{ "save" | i18n }}
</button>
</bit-section>
</ng-container>
</bit-container>

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { Component, OnDestroy, ViewChild } from "@angular/core";
import { Component, OnDestroy } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { from, lastValueFrom, switchMap } from "rxjs";
@@ -11,7 +11,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
@@ -22,7 +21,6 @@ import { DialogService, ToastService } from "@bitwarden/components";
import { FreeTrial } from "../../../core/types/free-trial";
import { TrialFlowService } from "../../services/trial-flow.service";
import { TaxInfoComponent } from "../../shared";
import {
AddCreditDialogResult,
openAddCreditDialog,
@@ -36,8 +34,6 @@ import {
templateUrl: "./organization-payment-method.component.html",
})
export class OrganizationPaymentMethodComponent implements OnDestroy {
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
organizationId: string;
isUnpaid = false;
accountCredit: number;
@@ -155,6 +151,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
data: {
initialPaymentMethod: this.paymentSource?.type,
organizationId: this.organizationId,
productTier: this.organization?.productTierType,
},
});
@@ -170,6 +167,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
data: {
initialPaymentMethod: this.paymentSource?.type,
organizationId: this.organizationId,
productTier: this.organization?.productTierType,
},
});
const result = await lastValueFrom(dialogRef.closed);
@@ -183,32 +181,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
}
};
protected updateTaxInformation = async (): Promise<void> => {
this.taxInfoComponent.taxFormGroup.updateValueAndValidity();
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
if (this.taxInfoComponent.taxFormGroup.invalid) {
return;
}
const request = new ExpandedTaxInfoUpdateRequest();
request.country = this.taxInfoComponent.country;
request.postalCode = this.taxInfoComponent.postalCode;
request.taxId = this.taxInfoComponent.taxId;
request.line1 = this.taxInfoComponent.line1;
request.line2 = this.taxInfoComponent.line2;
request.city = this.taxInfoComponent.city;
request.state = this.taxInfoComponent.state;
await this.billingApiService.updateOrganizationTaxInformation(this.organizationId, request);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("taxInfoUpdated"),
});
};
protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise<void> => {
await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request);
this.toastService.showToast({

View File

@@ -5,6 +5,8 @@ import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -104,6 +106,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private internalOrganizationService: InternalOrganizationServiceAbstraction,
) {}
ngOnInit() {
@@ -157,11 +160,20 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest
? this.formGroup.value.maxAutoscaleServiceAccounts
: null;
await this.organizationApiService.updateSecretsManagerSubscription(
const response = await this.organizationApiService.updateSecretsManagerSubscription(
this.organizationId,
request,
);
const organization = await this.internalOrganizationService.get(this.organizationId);
const organizationData = new OrganizationData(response, {
isMember: organization.isMember,
isProviderUser: organization.isProviderUser,
});
await this.internalOrganizationService.upsert(organizationData);
this.toastService.showToast({
variant: "success",
title: null,

View File

@@ -0,0 +1,142 @@
import { Injectable } from "@angular/core";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
export interface ResellerWarning {
type: "info" | "warning";
message: string;
}
@Injectable({ providedIn: "root" })
export class ResellerWarningService {
private readonly RENEWAL_WARNING_DAYS = 14;
private readonly GRACE_PERIOD_DAYS = 30;
constructor(private i18nService: I18nService) {}
getWarning(
organization: Organization,
organizationBillingMetadata: OrganizationBillingMetadataResponse,
): ResellerWarning | null {
if (!organization.hasReseller) {
return null; // If no reseller, return null immediately
}
// Check for past due warning first (highest priority)
if (this.shouldShowPastDueWarning(organizationBillingMetadata)) {
const gracePeriodEnd = this.getGracePeriodEndDate(organizationBillingMetadata.invoiceDueDate);
if (!gracePeriodEnd) {
return null;
}
return {
type: "warning",
message: this.i18nService.t(
"resellerPastDueWarning",
organization.providerName,
this.formatDate(gracePeriodEnd),
),
} as ResellerWarning;
}
// Check for open invoice warning
if (this.shouldShowInvoiceWarning(organizationBillingMetadata)) {
const invoiceCreatedDate = organizationBillingMetadata.invoiceCreatedDate;
const invoiceDueDate = organizationBillingMetadata.invoiceDueDate;
if (!invoiceCreatedDate || !invoiceDueDate) {
return null;
}
return {
type: "info",
message: this.i18nService.t(
"resellerOpenInvoiceWarning",
organization.providerName,
this.formatDate(organizationBillingMetadata.invoiceCreatedDate),
this.formatDate(organizationBillingMetadata.invoiceDueDate),
),
} as ResellerWarning;
}
// Check for renewal warning
if (this.shouldShowRenewalWarning(organizationBillingMetadata)) {
const subPeriodEndDate = organizationBillingMetadata.subPeriodEndDate;
if (!subPeriodEndDate) {
return null;
}
return {
type: "info",
message: this.i18nService.t(
"resellerRenewalWarning",
organization.providerName,
this.formatDate(organizationBillingMetadata.subPeriodEndDate),
),
} as ResellerWarning;
}
return null;
}
private shouldShowRenewalWarning(
organizationBillingMetadata: OrganizationBillingMetadataResponse,
): boolean {
if (
!organizationBillingMetadata.hasSubscription ||
!organizationBillingMetadata.subPeriodEndDate
) {
return false;
}
const renewalDate = new Date(organizationBillingMetadata.subPeriodEndDate);
const daysUntilRenewal = Math.ceil(
(renewalDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24),
);
return daysUntilRenewal <= this.RENEWAL_WARNING_DAYS;
}
private shouldShowInvoiceWarning(
organizationBillingMetadata: OrganizationBillingMetadataResponse,
): boolean {
if (
!organizationBillingMetadata.hasOpenInvoice ||
!organizationBillingMetadata.invoiceDueDate
) {
return false;
}
const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate);
return invoiceDueDate > new Date();
}
private shouldShowPastDueWarning(
organizationBillingMetadata: OrganizationBillingMetadataResponse,
): boolean {
if (
!organizationBillingMetadata.hasOpenInvoice ||
!organizationBillingMetadata.invoiceDueDate
) {
return false;
}
const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate);
return invoiceDueDate <= new Date() && !organizationBillingMetadata.isSubscriptionUnpaid;
}
private getGracePeriodEndDate(dueDate: Date | null): Date | null {
if (!dueDate) {
return null;
}
const gracePeriodEnd = new Date(dueDate);
gracePeriodEnd.setDate(gracePeriodEnd.getDate() + this.GRACE_PERIOD_DAYS);
return gracePeriodEnd;
}
private formatDate(date: Date | null): string {
if (!date) {
return "N/A";
}
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
});
}
}

View File

@@ -5,7 +5,12 @@
[showBankAccount]="!!organizationId"
[initialPaymentMethod]="initialPaymentMethod"
></app-payment-v2>
<app-tax-info (onCountryChanged)="onCountryChanged()"></app-tax-info>
<app-manage-tax-information
*ngIf="taxInformation"
[showTaxIdField]="showTaxIdField"
[startWith]="taxInformation"
(taxInformationChanged)="taxInformationChanged($event)"
/>
</ng-container>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary" [bitAction]="submit">

View File

@@ -1,22 +1,27 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { forwardRef, Component, Inject, ViewChild } from "@angular/core";
import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { TaxInfoComponent } from "../";
import { PaymentV2Component } from "../payment/payment-v2.component";
export interface AdjustPaymentDialogV2Params {
initialPaymentMethod?: PaymentMethodType;
organizationId?: string;
productTier?: ProductTierType;
}
export enum AdjustPaymentDialogV2ResultType {
@@ -27,9 +32,10 @@ export enum AdjustPaymentDialogV2ResultType {
@Component({
templateUrl: "./adjust-payment-dialog-v2.component.html",
})
export class AdjustPaymentDialogV2Component {
export class AdjustPaymentDialogV2Component implements OnInit {
@ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component;
@ViewChild(forwardRef(() => TaxInfoComponent)) taxInfoComponent: TaxInfoComponent;
@ViewChild(forwardRef(() => ManageTaxInformationComponent))
taxInfoComponent: ManageTaxInformationComponent;
protected readonly PaymentMethodType = PaymentMethodType;
protected readonly ResultType = AdjustPaymentDialogV2ResultType;
@@ -37,10 +43,14 @@ export class AdjustPaymentDialogV2Component {
protected dialogHeader: string;
protected initialPaymentMethod: PaymentMethodType;
protected organizationId?: string;
protected productTier?: ProductTierType;
protected taxInformation: TaxInformation;
constructor(
private apiService: ApiService,
private billingApiService: BillingApiServiceAbstraction,
private organizationApiService: OrganizationApiServiceAbstraction,
@Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogV2Params,
private dialogRef: DialogRef<AdjustPaymentDialogV2ResultType>,
private i18nService: I18nService,
@@ -50,10 +60,34 @@ export class AdjustPaymentDialogV2Component {
this.dialogHeader = this.i18nService.t(key);
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
this.organizationId = this.dialogParams.organizationId;
this.productTier = this.dialogParams.productTier;
}
onCountryChanged = () => {
if (this.taxInfoComponent.taxInfo.country === "US") {
ngOnInit(): void {
if (this.organizationId) {
this.organizationApiService
.getTaxInfo(this.organizationId)
.then((response: TaxInfoResponse) => {
this.taxInformation = TaxInformation.from(response);
})
.catch(() => {
this.taxInformation = new TaxInformation();
});
} else {
this.apiService
.getTaxInfo()
.then((response: TaxInfoResponse) => {
this.taxInformation = TaxInformation.from(response);
})
.catch(() => {
this.taxInformation = new TaxInformation();
});
}
}
taxInformationChanged(event: TaxInformation) {
this.taxInformation = event;
if (event.country === "US") {
this.paymentComponent.showBankAccount = !!this.organizationId;
} else {
this.paymentComponent.showBankAccount = false;
@@ -61,28 +95,34 @@ export class AdjustPaymentDialogV2Component {
this.paymentComponent.select(PaymentMethodType.Card);
}
}
};
}
submit = async (): Promise<void> => {
this.taxInfoComponent.taxFormGroup.updateValueAndValidity();
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
if (this.taxInfoComponent.taxFormGroup.invalid) {
if (!this.taxInfoComponent.validate()) {
return;
}
if (!this.organizationId) {
await this.updatePremiumUserPaymentMethod();
} else {
await this.updateOrganizationPaymentMethod();
try {
if (!this.organizationId) {
await this.updatePremiumUserPaymentMethod();
} else {
await this.updateOrganizationPaymentMethod();
}
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedPaymentMethod"),
});
this.dialogRef.close(AdjustPaymentDialogV2ResultType.Submitted);
} catch (error) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t(error.message) || error.message,
});
}
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedPaymentMethod"),
});
this.dialogRef.close(AdjustPaymentDialogV2ResultType.Submitted);
};
private updateOrganizationPaymentMethod = async () => {
@@ -90,27 +130,39 @@ export class AdjustPaymentDialogV2Component {
const request = new UpdatePaymentMethodRequest();
request.paymentSource = paymentSource;
request.taxInformation = {
country: this.taxInfoComponent.country,
postalCode: this.taxInfoComponent.postalCode,
taxId: this.taxInfoComponent.taxId,
line1: this.taxInfoComponent.line1,
line2: this.taxInfoComponent.line2,
city: this.taxInfoComponent.city,
state: this.taxInfoComponent.state,
};
request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation);
await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request);
};
protected get showTaxIdField(): boolean {
if (!this.organizationId) {
return false;
}
switch (this.productTier) {
case ProductTierType.Free:
case ProductTierType.Families:
return false;
default:
return true;
}
}
private updatePremiumUserPaymentMethod = async () => {
const { type, token } = await this.paymentComponent.tokenize();
const request = new PaymentRequest();
request.paymentMethodType = type;
request.paymentToken = token;
request.country = this.taxInfoComponent.country;
request.postalCode = this.taxInfoComponent.postalCode;
request.country = this.taxInformation.country;
request.postalCode = this.taxInformation.postalCode;
request.taxId = this.taxInformation.taxId;
request.state = this.taxInformation.state;
request.line1 = this.taxInformation.line1;
request.line2 = this.taxInformation.line2;
request.city = this.taxInformation.city;
request.state = this.taxInformation.state;
await this.apiService.postAccountPayment(request);
};

View File

@@ -5,7 +5,12 @@
>
<ng-container bitDialogContent>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
<app-manage-tax-information
*ngIf="taxInformation"
[showTaxIdField]="showTaxIdField"
[startWith]="taxInformation"
(taxInformationChanged)="taxInformationChanged($event)"
/>
</ng-container>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">

View File

@@ -1,19 +1,21 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, ViewChild } from "@angular/core";
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
import { FormGroup } from "@angular/forms";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PaymentComponent } from "../payment/payment.component";
import { TaxInfoComponent } from "../tax-info.component";
export interface AdjustPaymentDialogData {
organizationId: string;
@@ -28,9 +30,9 @@ export enum AdjustPaymentDialogResult {
@Component({
templateUrl: "adjust-payment-dialog.component.html",
})
export class AdjustPaymentDialogComponent {
export class AdjustPaymentDialogComponent implements OnInit {
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
@ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent;
organizationId: string;
currentType: PaymentMethodType;
@@ -39,6 +41,8 @@ export class AdjustPaymentDialogComponent {
protected DialogResult = AdjustPaymentDialogResult;
protected formGroup = new FormGroup({});
protected taxInformation: TaxInformation;
constructor(
private dialogRef: DialogRef,
@Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData,
@@ -52,26 +56,49 @@ export class AdjustPaymentDialogComponent {
this.currentType = data.currentType;
}
ngOnInit(): void {
if (this.organizationId) {
this.organizationApiService
.getTaxInfo(this.organizationId)
.then((response: TaxInfoResponse) => {
this.taxInformation = TaxInformation.from(response);
})
.catch(() => {
this.taxInformation = new TaxInformation();
});
} else {
this.apiService
.getTaxInfo()
.then((response: TaxInfoResponse) => {
this.taxInformation = TaxInformation.from(response);
})
.catch(() => {
this.taxInformation = new TaxInformation();
});
}
}
submit = async () => {
if (!this.taxInfoComponent?.taxFormGroup.valid && this.taxInfoComponent?.taxFormGroup.touched) {
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
if (!this.taxInfoComponent?.validate()) {
return;
}
const request = new PaymentRequest();
const response = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxFormGroup?.value.postalCode;
request.country = this.taxInfoComponent.taxFormGroup?.value.country;
request.postalCode = this.taxInformation?.postalCode;
request.country = this.taxInformation?.country;
request.taxId = this.taxInformation?.taxId;
if (this.organizationId == null) {
return this.apiService.postAccountPayment(request);
} else {
request.taxId = this.taxInfoComponent.taxFormGroup?.value.taxId;
request.state = this.taxInfoComponent.taxFormGroup?.value.state;
request.line1 = this.taxInfoComponent.taxFormGroup?.value.line1;
request.line2 = this.taxInfoComponent.taxFormGroup?.value.line2;
request.city = this.taxInfoComponent.taxFormGroup?.value.city;
request.state = this.taxInfoComponent.taxFormGroup?.value.state;
request.taxId = this.taxInformation?.taxId;
request.state = this.taxInformation?.state;
request.line1 = this.taxInformation?.line1;
request.line2 = this.taxInformation?.line2;
request.city = this.taxInformation?.city;
request.state = this.taxInformation?.state;
return this.organizationApiService.updatePayment(this.organizationId, request);
}
});
@@ -84,8 +111,9 @@ export class AdjustPaymentDialogComponent {
this.dialogRef.close(AdjustPaymentDialogResult.Adjusted);
};
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === "US") {
taxInformationChanged(event: TaxInformation) {
this.taxInformation = event;
if (event.country === "US") {
this.paymentComponent.hideBank = !this.organizationId;
} else {
this.paymentComponent.hideBank = true;
@@ -95,6 +123,10 @@ export class AdjustPaymentDialogComponent {
}
}
}
protected get showTaxIdField(): boolean {
return !!this.organizationId;
}
}
/**

View File

@@ -110,23 +110,5 @@
{{ "paymentChargedWithUnpaidSubscription" | i18n }}
</p>
</bit-section>
<bit-section *ngIf="forOrganization">
<h2 bitTypography="h2">{{ "taxInformation" | i18n }}</h2>
<p bitTypography="body1">{{ "taxInformationDesc" | i18n }}</p>
<div *ngIf="!org || loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<form *ngIf="org && !loading" [formGroup]="taxForm" [bitSubmit]="submitTaxInfo">
<app-tax-info></app-tax-info>
<button bitButton bitFormButton buttonType="primary" type="submit">
{{ "save" | i18n }}
</button>
</form>
</bit-section>
</ng-container>
</bit-container>

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { lastValueFrom } from "rxjs";
@@ -16,7 +16,6 @@ import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/mode
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { DialogService, ToastService } from "@bitwarden/components";
@@ -29,15 +28,12 @@ import {
AdjustPaymentDialogResult,
openAdjustPaymentDialog,
} from "./adjust-payment-dialog/adjust-payment-dialog.component";
import { TaxInfoComponent } from "./tax-info.component";
@Component({
templateUrl: "payment-method.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PaymentMethodComponent implements OnInit, OnDestroy {
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
loading = false;
firstLoaded = false;
billing: BillingPaymentResponse;
@@ -61,7 +57,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
]),
});
taxForm = this.formBuilder.group({});
launchPaymentModalAutomatically = false;
protected freeTrialData: FreeTrial;
@@ -72,7 +67,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
protected platformUtilsService: PlatformUtilsService,
private router: Router,
private location: Location,
private logService: LogService,
private route: ActivatedRoute,
private formBuilder: FormBuilder,
private dialogService: DialogService,
@@ -198,15 +192,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
await this.load();
};
submitTaxInfo = async () => {
await this.taxInfo.submitTaxInfo();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("taxInfoUpdated"),
});
};
determineOrgsWithUpcomingPaymentIssues() {
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
this.organization,
@@ -231,10 +216,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
return this.organizationId != null;
}
get headerClass() {
return this.forOrganization ? ["page-header"] : ["tabbed-header"];
}
get paymentSourceClasses() {
if (this.paymentSource == null) {
return [];

View File

@@ -13,51 +13,41 @@
</bit-select>
</bit-form-field>
</div>
<div [ngClass]="trialFlow ? 'tw-col-span-6' : 'tw-col-span-4'">
<div class="tw-col-span-6">
<bit-form-field>
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
<input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" />
</bit-form-field>
</div>
<div class="tw-col-span-6" *ngIf="showTaxIdCheckbox">
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="includeTaxId" />
<bit-label>{{ "includeVAT" | i18n }}</bit-label>
</bit-form-control>
<div class="tw-col-span-6" *ngIf="isTaxSupported">
<bit-form-field>
<bit-label>{{ "address1" | i18n }}</bit-label>
<input bitInput type="text" formControlName="line1" autocomplete="address-line1" />
</bit-form-field>
</div>
</div>
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showTaxIdFields">
<div class="tw-col-span-6">
<div class="tw-col-span-6" *ngIf="isTaxSupported">
<bit-form-field>
<bit-label>{{ "address2" | i18n }}</bit-label>
<input bitInput type="text" formControlName="line2" autocomplete="address-line2" />
</bit-form-field>
</div>
<div class="tw-col-span-6" *ngIf="isTaxSupported">
<bit-form-field>
<bit-label for="addressCity">{{ "cityTown" | i18n }}</bit-label>
<input bitInput type="text" formControlName="city" autocomplete="address-level2" />
</bit-form-field>
</div>
<div class="tw-col-span-6" *ngIf="isTaxSupported">
<bit-form-field>
<bit-label>{{ "stateProvince" | i18n }}</bit-label>
<input bitInput type="text" formControlName="state" autocomplete="address-level1" />
</bit-form-field>
</div>
<div class="tw-col-span-6" *ngIf="isTaxSupported && showTaxIdField">
<bit-form-field>
<bit-label>{{ "taxIdNumber" | i18n }}</bit-label>
<input bitInput type="text" formControlName="taxId" />
</bit-form-field>
</div>
</div>
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showTaxIdFields">
<div class="tw-col-span-6">
<bit-form-field>
<bit-label>{{ "address1" | i18n }}</bit-label>
<input bitInput type="text" formControlName="line1" autocomplete="address-line1" />
</bit-form-field>
</div>
<div class="tw-col-span-6">
<bit-form-field>
<bit-label>{{ "address2" | i18n }}</bit-label>
<input bitInput type="text" formControlName="line2" autocomplete="address-line2" />
</bit-form-field>
</div>
<div class="tw-col-span-6">
<bit-form-field>
<bit-label for="addressCity">{{ "cityTown" | i18n }}</bit-label>
<input bitInput type="text" formControlName="city" autocomplete="address-level2" />
</bit-form-field>
</div>
<div class="tw-col-span-6">
<bit-form-field>
<bit-label>{{ "stateProvince" | i18n }}</bit-label>
<input bitInput type="text" formControlName="state" autocomplete="address-level1" />
</bit-form-field>
</div>
</div>
</form>

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