diff --git a/.eslintrc.json b/.eslintrc.json index c606b8f933b..3fd6dec3d7e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,6 +56,7 @@ "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }], + "@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }], "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "no-console": "error", "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package. @@ -141,8 +142,7 @@ } ] } - ], - "no-restricted-imports": ["error", { "patterns": ["src/**/*"] }] + ] } }, { @@ -164,147 +164,6 @@ "tailwindcss/no-contradicting-classname": "error" } }, - { - "files": ["libs/admin-console/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/admin-console/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/angular/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/angular/*", "src/**/*"] }] - } - }, - { - "files": ["libs/auth/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/auth/*", "src/**/*"] }] - } - }, - { - "files": ["libs/billing/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/billing/*", "src/**/*"] }] - } - }, - { - "files": ["libs/common/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/common/*", "src/**/*"] }] - } - }, - { - "files": ["libs/components/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/components/*", "src/**/*", "@bitwarden/angular/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/components/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-components/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/core/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-core/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/history/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-history/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/legacy/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-legacy/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/navigation/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-navigation/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/export/vault-export/vault-export-core/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/vault-export-core/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/export/vault-export/vault-export-ui/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/vault-export-ui/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/importer/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/importer/*", "src/**/*"] }] - } - }, - { - "files": ["libs/node/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/node/*", "src/**/*"] }] - } - }, - { - "files": ["libs/platform/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }] - } - }, - { - "files": ["libs/tools/send/send-ui/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }] - } - }, - { - "files": ["libs/tools/card/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/tools-card/*", "src/**/*"] }] - } - }, - { - "files": ["libs/vault/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/vault/*", "src/**/*"] }] - } - }, { "files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"], "excludedFiles": [ @@ -344,10 +203,30 @@ ] } }, + { + "files": ["**/src/**/*.ts"], + "excludedFiles": ["**/platform/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "**/platform/**/internal", // General internal pattern + // All features that have been converted to barrel files + "**/platform/messaging/**", + "**/src/**/*" // Prevent relative imports across libs. + ] + } + ] + } + }, { "files": ["bitwarden_license/bit-common/src/**/*.ts"], "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/bit-common/*", "src/**/*"] }] + "no-restricted-imports": [ + "error", + { "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] } + ] } }, { @@ -357,7 +236,12 @@ "no-restricted-imports": [ "error", { - "patterns": ["biwarden_license/**", "@bitwarden/bit-common/*", "@bitwarden/bit-web/*"] + "patterns": [ + "biwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + "**/src/**/*" + ] } ], // Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cb36d87b9e1..91cd174a6b6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -111,6 +111,7 @@ apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bit ## Component Library ## .storybook @bitwarden/team-design-system libs/components @bitwarden/team-design-system +libs/ui @bitwarden/team-design-system apps/browser/src/platform/popup/layout @bitwarden/team-design-system apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-design-system apps/web/src/app/layouts @bitwarden/team-design-system @@ -161,3 +162,7 @@ apps/web/src/locales/en/messages.json **/*.Dockerfile **/.dockerignore **/entrypoint.sh + +## Overrides +# tsconfig files are potentially dangerous and will be reviewed by platform to prevent misconfigurations +**/tsconfig.json @bitwarden/team-platform-dev diff --git a/.github/renovate.json b/.github/renovate.json index 76a52136ae7..b5c43cc1d39 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -4,13 +4,9 @@ "enabledManagers": ["cargo", "github-actions", "npm"], "packageRules": [ { - "groupName": "gh minor", + "groupName": "github-action minor", "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor", "patch"] - }, - { - "matchManagers": ["github-actions"], - "commitMessagePrefix": "[deps] BRE:" + "matchUpdateTypes": ["minor"] }, { "matchManagers": ["cargo"], @@ -73,6 +69,29 @@ "commitMessagePrefix": "[deps] Auth:", "reviewers": ["team:team-auth-dev"] }, + { + "matchPackageNames": [ + "@angular-eslint/eslint-plugin-template", + "@angular-eslint/eslint-plugin", + "@angular-eslint/schematics", + "@angular-eslint/template-parser", + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "eslint-config-prettier", + "eslint-import-resolver-typescript", + "eslint-plugin-import", + "eslint-plugin-rxjs-angular", + "eslint-plugin-rxjs", + "eslint-plugin-storybook", + "eslint-plugin-tailwindcss", + "eslint", + "husky", + "lint-staged" + ], + "description": "Architecture owned dependencies", + "commitMessagePrefix": "[deps] Architecture:", + "reviewers": ["team:dept-architecture"] + }, { "matchPackageNames": [ "@emotion/css", @@ -86,6 +105,7 @@ "prettier", "prettier-plugin-tailwindcss", "rimraf", + "@storybook/web-components-webpack5", "tabbable", "tldts", "wait-on" @@ -104,6 +124,8 @@ "matchPackageNames": [ "@babel/core", "@babel/preset-env", + "@bitwarden/sdk-internal", + "@electron/fuses", "@electron/notarize", "@electron/rebuild", "@ngtools/webpack", @@ -115,7 +137,7 @@ "@types/node", "@types/node-forge", "@types/node-ipc", - "@yao-pkg", + "@yao-pkg/pkg", "babel-loader", "browserslist", "copy-webpack-plugin", @@ -135,6 +157,7 @@ "tsconfig-paths-webpack-plugin", "type-fest", "typescript", + "typescript-strict-plugin", "webpack", "webpack-cli", "webpack-dev-server", @@ -151,12 +174,13 @@ "@angular/cdk", "@angular/cli", "@angular/common", - "@angular/compiler", "@angular/compiler-cli", + "@angular/compiler", "@angular/core", "@angular/forms", + "@angular/platform-browser-dynamic", + "@angular/platform-browser", "@angular/platform", - "@angular/compiler", "@angular/router", "@compodoc/compodoc", "@ng-select/ng-select", @@ -164,8 +188,11 @@ "@storybook/addon-actions", "@storybook/addon-designs", "@storybook/addon-essentials", + "@storybook/addon-interactions", "@storybook/addon-links", "@storybook/angular", + "@storybook/manager-api", + "@storybook/theming", "@types/react", "autoprefixer", "bootstrap", @@ -186,25 +213,11 @@ }, { "matchPackageNames": [ - "@angular-eslint/eslint-plugin", - "@angular-eslint/eslint-plugin-template", - "@angular-eslint/template-parser", "@types/jest", - "@typescript-eslint/eslint-plugin", - "@typescript-eslint/parser", - "eslint", - "eslint-config-prettier", - "eslint-import-resolver-typescript", - "eslint-plugin-import", - "eslint-plugin-rxjs", - "eslint-plugin-rxjs-angular", - "eslint-plugin-storybook", - "eslint-plugin-tailwindcss", - "husky", "jest-junit", "jest-mock-extended", "jest-preset-angular", - "lint-staged", + "jest-diff", "ts-jest" ], "description": "Secrets Manager owned dependencies", diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 73d323851e5..653f6591c7f 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -3,26 +3,12 @@ ./apps/browser/src/safari/desktop/Assets.xcassets/AppIcon.appiconset ./apps/browser/src/safari/desktop/Base.lproj ./apps/browser/store/windows/Assets -./bitwarden_license/README.md ./libs/angular/src/directives/cipherListVirtualScroll.directive.ts -./libs/admin-console/README.md -./libs/auth/README.md -./libs/billing/README.md -./libs/common/src/tools/integration/README.md -./libs/platform/README.md -./libs/key-management/README.md -./libs/tools/README.md -./libs/tools/export/vault-export/README.md -./libs/tools/send/README.md -./libs/tools/card/README.md -./libs/vault/README.md -./README.md ./LICENSE_BITWARDEN.txt ./CONTRIBUTING.md ./LICENSE_GPL.txt ./LICENSE.txt ./apps/web/Dockerfile -./apps/web/README.md ./apps/desktop/resources/installerSidebar.bmp ./apps/desktop/resources/appx/SplashScreen.png ./apps/desktop/resources/appx/BadgeLogo.png @@ -30,10 +16,7 @@ ./apps/desktop/resources/appx/StoreLogo.png ./apps/desktop/resources/appx/Wide310x150Logo.png ./apps/desktop/resources/appx/Square44x44Logo.png -./apps/desktop/README.md ./apps/cli/stores/chocolatey/tools/VERIFICATION.txt -./apps/cli/README.md -./apps/browser/README.md ./apps/browser/store/windows/AppxManifest.xml ./apps/browser/src/background/nativeMessaging.background.ts ./apps/browser/src/models/browserComponentState.ts diff --git a/.github/workflows/brew-bump-desktop.yml b/.github/workflows/brew-bump-desktop.yml deleted file mode 100644 index 1b3c99128bf..00000000000 --- a/.github/workflows/brew-bump-desktop.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Bump Desktop Cask - -on: - push: - tags: - - desktop-v** - workflow_dispatch: - -defaults: - run: - shell: bash - -jobs: - update-desktop-cask: - name: Update Bitwarden Desktop Cask - runs-on: macos-13 - steps: - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "brew-bump-workflow-pat" - - - name: Update Homebrew cask - uses: macauley/action-homebrew-bump-cask@445c42390d790569d938f9068d01af39ca030feb # v1.0.0 - with: - # Required, custom GitHub access token with the 'public_repo' and 'workflow' scopes - token: ${{ steps.retrieve-secrets.outputs.brew-bump-workflow-pat }} - org: bitwarden - tap: Homebrew/homebrew-cask - cask: bitwarden - tag: ${{ github.ref }} - revision: ${{ github.sha }} - force: true - dryrun: true diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index aa62d602ad8..c9e9f588c83 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -169,7 +169,7 @@ jobs: zip -r browser-source.zip browser-source - name: Upload browser source - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -196,11 +196,7 @@ jobs: - 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" + artifact_name: "dist-edge-MV3" - name: "firefox" npm_command: "dist:firefox" archive_name: "dist-firefox.zip" @@ -272,7 +268,7 @@ jobs: working-directory: browser-source/apps/browser - name: Upload extension artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ matrix.artifact_name }}-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/${{ matrix.archive_name }} @@ -409,7 +405,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-safari.zip diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 02432e0a5f4..d5d78811cda 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 3221c7eef2f..ca681dac6b0 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -232,42 +232,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: com.bitwarden.desktop.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -428,91 +428,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -918,28 +918,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -1166,7 +1166,7 @@ jobs: run: npm run pack:mac:mas - name: Upload .pkg artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -1196,6 +1196,8 @@ jobs: uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0 with: channel-id: C074F5UESQ0 + method: chat.postMessage + token: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }} payload: | { "blocks": [ @@ -1209,7 +1211,6 @@ jobs: ] } env: - SLACK_BOT_TOKEN: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }} BUILD_NUMBER: ${{ needs.setup.outputs.build_number }} @@ -1424,7 +1425,7 @@ jobs: zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 73ae0e14962..b7e8a51897c 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -164,7 +164,7 @@ jobs: run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build - name: Upload ${{ matrix.name }} artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: context: apps/web file: apps/web/Dockerfile @@ -310,7 +310,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index a5ebd363f63..75a07431942 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -56,7 +56,7 @@ jobs: run: npm run build-storybook:ci - name: Publish to Chromatic - uses: chromaui/action@64a9c0ca3bfb724389b0d536e544f56b7b5ff5b3 # v11.20.2 + uses: chromaui/action@8a12962215a66cd05b1ac5b0f1c08768d1aab155 # v11.25.0 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index b5e84ff875b..9b890491282 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -63,14 +63,14 @@ jobs: runs-on: ubuntu-22.04 outputs: environment: ${{ steps.config.outputs.environment }} - environment-url: ${{ steps.config.outputs.environment-url }} - environment-name: ${{ steps.config.outputs.environment-name }} - environment-artifact: ${{ steps.config.outputs.environment-artifact }} - azure-login-creds: ${{ steps.config.outputs.azure-login-creds }} - retrieve-secrets-keyvault: ${{ steps.config.outputs.retrieve-secrets-keyvault }} - sync-utility: ${{ steps.config.outputs.sync-utility }} - sync-delete-destination-files: ${{ steps.config.outputs.sync-delete-destination-files }} - slack-channel-name: ${{ steps.config.outputs.slack-channel-name }} + environment_url: ${{ steps.config.outputs.environment_url }} + environment_name: ${{ steps.config.outputs.environment_name }} + environment_artifact: ${{ steps.config.outputs.environment_artifact }} + azure_login_creds: ${{ steps.config.outputs.azure_login_creds }} + retrive_secrets_keyvault: ${{ steps.config.outputs.retrive_secrets_keyvault }} + sync_utility: ${{ steps.config.outputs.sync_utility }} + sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }} + slack_channel_name: ${{ steps.config.outputs.slack_channel_name }} steps: - name: Configure id: config @@ -81,48 +81,48 @@ jobs: case ${{ inputs.environment }} in "USQA") - echo "azure-login-creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "EUQA") - echo "azure-login-creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "USPROD") - echo "azure-login-creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "EUPROD") - echo "azure-login-creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "USDEV") - echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-dev" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT ;; esac # Set the sync utility to use for deployment to the environment (az-sync or azcopy) - echo "sync-utility=azcopy" >> $GITHUB_OUTPUT + echo "sync_utility=azcopy" >> $GITHUB_OUTPUT - name: Environment Protection env: @@ -168,10 +168,10 @@ jobs: fi approval: - name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }} + name: Approval for Deployment to ${{ needs.setup.outputs.environment_name }} needs: setup runs-on: ubuntu-22.04 - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} steps: - name: Success Code run: exit 0 @@ -181,9 +181,9 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} outputs: - artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} + artifact_build_commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' if: ${{ inputs.build-web-run-id }} @@ -242,7 +242,7 @@ jobs: run: | # If run-id was used, get the commit from the download-latest-artifacts-run-id step if [ "${{ inputs.build-web-run-id }}" ]; then - echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. @@ -251,7 +251,7 @@ jobs: else # Set the commit to the output of step download-latest-artifacts. - echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT fi notify-start: @@ -271,11 +271,11 @@ jobs: id: slack-message with: project: Clients - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} tag: ${{ inputs.branch-or-tag }} - slack-channel: ${{ needs.setup.outputs.slack-channel-name }} + slack-channel: ${{ needs.setup.outputs.slack_channel_name }} event: 'start' - commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} + commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -287,7 +287,7 @@ jobs: - name: Display commit SHA run: | REPO_URL="https://github.com/bitwarden/clients/commit" - COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}" + COMMIT_SHA="${{ needs.artifact-check.outputs.artifact_build_commit }}" echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY azure-deploy: @@ -299,9 +299,9 @@ jobs: runs-on: ubuntu-22.04 env: _ENVIRONMENT: ${{ needs.setup.outputs.environment }} - _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment-url }} - _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment-name }} - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} + _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment_url }} + _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment_name }} + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 @@ -309,31 +309,31 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' - ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} + ref: ${{ needs.artifact-check.outputs.artifact_build_commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: - creds: ${{ secrets[needs.setup.outputs.azure-login-creds] }} + creds: ${{ secrets[needs.setup.outputs.azure_login_creds] }} - name: Retrieve Storage Account connection string for az sync - if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }} + if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} id: retrieve-secrets-az-sync uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} + keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-dev-key-temp" - name: Retrieve Storage Account name and SPN credentials for azcopy - if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }} + if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} id: retrieve-secrets-azcopy uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} + keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' @@ -363,7 +363,7 @@ jobs: run: unzip ${{ env._ENVIRONMENT_ARTIFACT }} - name: Sync to Azure Storage Account using az storage blob sync - if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }} + if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} working-directory: apps/web run: | az storage blob sync \ @@ -373,7 +373,7 @@ jobs: --delete-destination=${{ inputs.force-delete-destination }} - name: Sync to Azure Storage Account using azcopy - if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }} + if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} working-directory: apps/web env: AZCOPY_AUTO_LOGIN_TYPE: SPN @@ -397,7 +397,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} state: 'success' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -406,7 +406,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} state: 'failure' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -424,11 +424,11 @@ jobs: uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: project: Clients - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} tag: ${{ inputs.branch-or-tag }} slack-channel: ${{ needs.notify-start.outputs.channel_id }} event: ${{ needs.azure-deploy.result }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} + commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} update-ts: ${{ needs.notify-start.outputs.ts }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a907618bd36..5bc566202c6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -34,6 +34,7 @@ jobs: ! -path "*/.DS_Store" \ ! -path "*/*locales/*" \ ! -path "./.github/*" \ + ! -path "*/README.md" \ ! -path "*/Cargo.toml" \ ! -path "*/Cargo.lock" \ ! -path "./apps/desktop/macos/*" \ @@ -54,21 +55,25 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ steps.retrieve-node-version.outputs.node_version }} + - name: Install Node dependencies + run: npm ci + + - name: Lint unowned dependencies + run: npm run lint:dep-ownership + - name: Run linter - run: | - npm ci - npm run lint + run: npm run lint rust: name: Run Rust lint on ${{ matrix.os }} - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + runs-on: ${{ matrix.os || 'ubuntu-24.04' }} strategy: matrix: os: - - ubuntu-latest - - macos-latest - - windows-latest + - ubuntu-24.04 + - macos-14 + - windows-2022 steps: - name: Checkout repo diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 0a561306797..d758e6f11c9 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -43,8 +43,8 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version-output.outputs.version }} - deployment-id: ${{ steps.deployment.outputs.deployment_id }} + release_version: ${{ steps.version-output.outputs.version }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} defaults: run: working-directory: . @@ -88,7 +88,7 @@ jobs: needs: setup if: inputs.snap_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -125,7 +125,7 @@ jobs: needs: setup if: inputs.choco_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -165,7 +165,7 @@ jobs: needs: setup if: inputs.npm_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -222,7 +222,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -230,4 +230,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 5ef378ad439..ae631165db9 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -7,19 +7,18 @@ on: publish_type: description: 'Publish Options' required: true - default: 'Initial Publish' + default: 'Publish' type: choice options: - - Initial Publish - - Republish + - Publish - Dry Run version: description: 'Version to publish (default: latest desktop release)' required: true type: string default: latest - rollout_percentage: - description: 'Staged Rollout Percentage' + electron_rollout_percentage: + description: 'Staged Rollout Percentage for Electron' required: true default: '10' type: string @@ -39,10 +38,10 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} - tag-name: ${{ steps.version.outputs.tag_name }} - deployment-id: ${{ steps.deployment.outputs.deployment_id }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} + tag_name: ${{ steps.version.outputs.tag_name }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} steps: - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -76,7 +75,7 @@ jobs: fi - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -100,7 +99,7 @@ jobs: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' environment: 'Desktop - Production' - description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release-channel.outputs.channel }} from branch ${{ github.ref_name }}' + description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release_channel.outputs.channel }} from branch ${{ github.ref_name }}' task: release electron-blob: @@ -108,8 +107,8 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -136,8 +135,8 @@ jobs: - name: Set staged rollout percentage env: - RELEASE_CHANNEL: ${{ needs.setup.outputs.release-channel }} - ROLLOUT_PCT: ${{ inputs.rollout_percentage }} + RELEASE_CHANNEL: ${{ needs.setup.outputs.release_channel }} + ROLLOUT_PCT: ${{ inputs.electron_rollout_percentage }} run: | echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml @@ -163,7 +162,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -171,7 +170,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} snap: name: Deploy Snap @@ -179,8 +178,8 @@ jobs: needs: setup if: inputs.snap_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -223,8 +222,8 @@ jobs: needs: setup if: inputs.choco_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -284,7 +283,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -292,4 +291,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 75442187516..498f8748959 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} + release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -118,7 +118,7 @@ jobs: - name: Rename build artifacts env: - PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} run: | mv browser-source.zip browser-source-$PACKAGE_VERSION.zip mv dist-chrome.zip dist-chrome-$PACKAGE_VERSION.zip @@ -128,16 +128,16 @@ jobs: - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: - artifacts: 'browser-source-${{ needs.setup.outputs.release-version }}.zip, - dist-chrome-${{ needs.setup.outputs.release-version }}.zip, - dist-opera-${{ needs.setup.outputs.release-version }}.zip, - dist-firefox-${{ needs.setup.outputs.release-version }}.zip, - dist-edge-${{ needs.setup.outputs.release-version }}.zip' + artifacts: 'browser-source-${{ needs.setup.outputs.release_version }}.zip, + dist-chrome-${{ needs.setup.outputs.release_version }}.zip, + dist-opera-${{ needs.setup.outputs.release_version }}.zip, + dist-firefox-${{ needs.setup.outputs.release_version }}.zip, + dist-edge-${{ needs.setup.outputs.release_version }}.zip' commit: ${{ github.sha }} - tag: "browser-v${{ needs.setup.outputs.release-version }}" - name: "Browser v${{ needs.setup.outputs.release-version }}" + tag: "browser-v${{ needs.setup.outputs.release_version }}" + name: "Browser v${{ needs.setup.outputs.release_version }}" body: "" token: ${{ secrets.GITHUB_TOKEN }} draft: true diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 05c53f9752d..6a10bec1ba2 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} + release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -73,9 +73,9 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 env: - PKG_VERSION: ${{ needs.setup.outputs.release-version }} + PKG_VERSION: ${{ needs.setup.outputs.release_version }} with: artifacts: "apps/cli/bw-oss-windows-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-windows-sha256-${{ env.PKG_VERSION }}.txt, diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 3ec11c77852..a5e374395d8 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -16,9 +16,9 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} - branch-name: ${{ steps.branch.outputs.branch-name }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} + branch_name: ${{ steps.branch.outputs.branch_name }} build_number: ${{ steps.increment-version.outputs.build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: @@ -63,7 +63,7 @@ jobs: echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -102,7 +102,7 @@ jobs: git push -u origin $branch_name - echo "branch-name=$branch_name" >> $GITHUB_OUTPUT + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT - name: Get Node Version id: retrieve-node-version @@ -116,7 +116,7 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -126,7 +126,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -158,45 +158,45 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: ${{ needs.setup.outputs.release-channel }}-linux.yml - path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml + name: ${{ needs.setup.outputs.release_channel }}-linux.yml + path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml if-no-files-found: error @@ -209,14 +209,14 @@ jobs: shell: pwsh working-directory: apps/desktop env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -299,94 +299,94 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: ${{ needs.setup.outputs.release-channel }}.yml - path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml + name: ${{ needs.setup.outputs.release_channel }}.yml + path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml if-no-files-found: error @@ -395,7 +395,7 @@ jobs: runs-on: macos-13 needs: setup env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -405,7 +405,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -529,7 +529,7 @@ jobs: - setup - macos-build env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -539,7 +539,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -707,31 +707,31 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: ${{ needs.setup.outputs.release-channel }}-mac.yml - path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml + name: ${{ needs.setup.outputs.release_channel }}-mac.yml + path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml if-no-files-found: error @@ -742,7 +742,7 @@ jobs: - setup - macos-build env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -752,7 +752,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -915,7 +915,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Upload .pkg artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -939,7 +939,7 @@ jobs: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' environment: 'Desktop - Beta' - description: 'Deployment ${{ needs.setup.outputs.release-version }} to channel ${{ needs.setup.outputs.release-channel }} from branch ${{ needs.setup.outputs.branch-name }}' + description: 'Deployment ${{ needs.setup.outputs.release_version }} to channel ${{ needs.setup.outputs.release_channel }} from branch ${{ needs.setup.outputs.branch_name }}' task: release - name: Login to Azure @@ -963,7 +963,7 @@ jobs: - name: Rename .pkg to .pkg.archive env: - PKG_VERSION: ${{ needs.setup.outputs.release-version }} + PKG_VERSION: ${{ needs.setup.outputs.release_version }} working-directory: apps/desktop/artifacts run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive @@ -1020,5 +1020,5 @@ jobs: git config --global url."https://".insteadOf ssh:// - name: Remove branch env: - BRANCH: ${{ needs.setup.outputs.branch-name }} + BRANCH: ${{ needs.setup.outputs.branch_name }} run: git push origin --delete $BRANCH diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index d9394347f60..57143747a86 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -22,8 +22,8 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -49,7 +49,7 @@ jobs: monorepo-project: desktop - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -96,11 +96,11 @@ jobs: file_path: "apps/desktop/artifacts/sha256-checksums.txt" - name: Create Release - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 - if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 + if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} env: PKG_VERSION: ${{ steps.version.outputs.version }} - RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} + RELEASE_CHANNEL: ${{ steps.release_channel.outputs.channel }} with: artifacts: "apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-amd64.deb, apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x86_64.rpm, diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index faa398f6d67..0301b814796 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -81,7 +81,7 @@ jobs: - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: name: "Web v${{ needs.setup.outputs.release_version }}" commit: ${{ github.sha }} diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index a914a2c4a7a..ac2733e765b 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -461,6 +461,7 @@ jobs: - name: Check out main branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + fetch-depth: 0 ref: main token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index b0874b38cbf..ac7f0ae6f71 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: cx_result.sarif @@ -66,10 +66,9 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with SonarCloud - uses: sonarsource/sonarcloud-github-action@02ef91109b2d589e757aefcfb2854c2783fd7b19 # v4.0.0 + uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: args: > -Dsonar.organization=${{ github.repository_owner }} diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index 91250a443f2..4ec3af3be97 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -1,4 +1,5 @@ name: Staged Rollout Desktop +run-name: Staged Rollout Desktop - ${{ inputs.rollout_percentage }}% on: workflow_dispatch: diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 6caa7b99331..abb292f53f3 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: 'Run stale action' - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: stale-issue-label: 'needs-reply' stale-pr-label: 'needs-changes' diff --git a/angular.json b/angular.json index 7053050262e..665d810cf4e 100644 --- a/angular.json +++ b/angular.json @@ -147,6 +147,10 @@ "./tsconfig.json", "-e", "json", + "--disableInternal", + "--disableLifeCycleHooks", + "--disablePrivate", + "--disableProtected", "-d", ".", "--disableRoutesGraph" @@ -165,6 +169,10 @@ "./tsconfig.json", "-e", "json", + "--disableInternal", + "--disableLifeCycleHooks", + "--disablePrivate", + "--disableProtected", "-d", ".", "--disableRoutesGraph" diff --git a/apps/browser/package.json b/apps/browser/package.json index 3adeb292b6d..8fc1d733921 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,35 +1,35 @@ { "name": "@bitwarden/browser", - "version": "2025.1.0", + "version": "2025.1.4", "scripts": { "build": "npm run build:chrome", - "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", - "build:edge": "cross-env BROWSER=edge webpack", - "build:firefox": "cross-env BROWSER=firefox webpack", - "build:opera": "cross-env BROWSER=opera webpack", - "build:safari": "cross-env BROWSER=safari webpack", + "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:opera": "cross-env BROWSER=opera NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:safari": "cross-env BROWSER=safari NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:watch": "npm run build:watch:chrome", "build:watch:chrome": "npm run build:chrome -- --watch", "build:watch:edge": "npm run build:edge -- --watch", "build:watch:firefox": "npm run build:firefox -- --watch", "build:watch:opera": "npm run build:opera -- --watch", "build:watch:safari": "npm run build:safari -- --watch", - "build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:chrome", - "build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:edge", - "build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:firefox", - "build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:opera", - "build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:safari", + "build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome", + "build:prod:edge": "cross-env NODE_ENV=production npm run build:edge", + "build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox", + "build:prod:opera": "cross-env NODE_ENV=production npm run build:opera", + "build:prod:safari": "cross-env NODE_ENV=production npm run build:safari", "dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.ps1 dist-chrome.zip", "dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.ps1 dist-edge.zip", "dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.ps1 dist-firefox.zip", "dist:opera": "npm run build:prod:opera && mkdir -p dist && ./scripts/compress.ps1 dist-opera.zip", "dist:safari": "npm run build:prod:safari && ./scripts/package-safari.ps1", - "dist:edge:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:edge", "dist:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:firefox", "dist:opera:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:opera", "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari", "test": "jest", "test:watch": "jest --watch", - "test:watch:all": "jest --watchAll" + "test:watch:all": "jest --watchAll", + "test:clearCache": "jest --clear-cache" } } diff --git a/apps/browser/postcss.config.js b/apps/browser/postcss.config.js index c4513687e89..83e237f06e5 100644 --- a/apps/browser/postcss.config.js +++ b/apps/browser/postcss.config.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef */ +/* eslint-disable no-undef, @typescript-eslint/no-require-imports */ module.exports = { plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")], }; diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 46f119bdafe..fb618e49d31 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "توليد عبارة المرور" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "إعادة توليد كلمة المرور" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "قم بتأكيد هويتك" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "خزانتك مقفلة. قم بتأكيد هويتك للمتابعة." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "اطلب إضافة عنصر إذا لم يتم العثور على عنصر في المخزن الخاص بك. ينطبق على جميع حسابات تسجيل الدخول." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "أظهر البطاقات في صفحة التبويبات" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "إظهار الهويات على صفحة التبويب" @@ -2324,6 +2342,9 @@ "message": "النطاقات", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "النطاقات المستبعدة" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطافات لجميع الحسابات مسجلة الدخول. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "الموقع $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "تم حفظ تغييرات استبعاد النطاقات" }, @@ -2789,6 +2822,20 @@ "error": { "message": "خطأ" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "إنشاء اسم المستخدم" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "استيراد بياناتك إلى Bitwarden؟", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "حماية بياناتك الأخيرة واستيرادك إلى Bitwarden؟", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "حفظ كملف غير مشفر", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "استيراد إلى Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "جارِ الاستيراد...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "لقد تم استيراد البيانات بنجاح!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "خطأ في الاستيراد. تحقق من وحدة التحكم للحصول على التفاصيل.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "تم مواجهة خطأ في الشبكة أثناء الاستيراد.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "الاسم البديل للنطاق" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 51bfe95a48a..c5875377ecd 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Keçid ifadələri yarat" }, + "passwordGenerated": { + "message": "Parol yaradıldı." + }, + "passphraseGenerated": { + "message": "Keçid ifadəsi yaradıldı" + }, + "usernameGenerated": { + "message": "İstifadəçi adı yaradıldı" + }, + "emailGenerated": { + "message": "E-poçt yaradıldı" + }, "regeneratePassword": { "message": "Parolu yenidən yarat" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Kimliyi doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "yourVaultIsLocked": { "message": "Seyfiniz kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Seyfinizdə tapılmayan elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." }, - "showCardsInVaultView": { - "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showCardsInVaultViewV2": { + "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showCardsCurrentTab": { "message": "Kartları Vərəq səhifəsində göstər" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Asan avto-doldurma üçün Vərəq səhifəsində kart elementlərini sadalayın." }, - "showIdentitiesInVaultView": { - "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showIdentitiesInVaultViewV2": { + "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showIdentitiesCurrentTab": { "message": "Vərəq səhifəsində kimlikləri göstər" @@ -2324,6 +2342,9 @@ "message": "Domenlər", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Əngəllənmiş domenlər" + }, "excludedDomains": { "message": "İstisna edilən domenlər" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, giriş etmiş bütün hesablar üçün bu domenlərin giriş detallarını saxlamağı soruşmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, + "blockedDomainsDesc": { + "message": "Bu veb saytlar üçün avto-doldurma və digər əlaqəli özəlliklər təklif olunmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." + }, + "autofillBlockedNoticeV2": { + "message": "Bu veb sayt üçün avto-doldurma əngəllənib." + }, + "autofillBlockedNoticeGuidance": { + "message": "Bunu ayarlarda dəyişdir" + }, "websiteItemLabel": { "message": "Veb sayt $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Əngəllənmiş domen dəyişiklikləri saxlanıldı" + }, "excludedDomainsSavedSuccess": { "message": "İstisna domen dəyişikliyi saxlanıldı" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Xəta" }, + "decryptionError": { + "message": "Şifrə açma xətası" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden, aşağıda sadalanan seyf element(lər)inin şifrəsini aça bilmədi." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Əlavə data itkisini önləmək üçün", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "müştəri dəstəyi ilə əlaqə saxlayın.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "İstifadəçi adı yarat" }, @@ -3396,38 +3443,6 @@ "message": "Yığcamlaşdırmanı aç/bağla", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Datanız Bitwarden-ə köçürülsün?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass datanızı qoruyub Bitwarden-ə köçürürsünüz?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Şifrələnməmiş fayl olaraq saxla", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden-ə köçür", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Daxilə köçürülür...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data uğurla daxilə köçürüldü!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Daxilə köçürmə xətası. Detallar üçün konsolu yoxlayın.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Daxilə köçürmə zamanı şəbəkə xətası baş verdi.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domen ləqəbi" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Avto-doldurma təklifləri" }, + "itemSuggestions": { + "message": "Təklif olunan elementlər" + }, "autofillSuggestionsTip": { "message": "Bu saytın avto-doldurması üçün giriş elementini saxlayın" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Mətn \"Send\"ləri" }, - "bitwardenNewLook": { - "message": "Bitwarden-in yeni bir görünüşü var!" - }, - "bitwardenNewLookDesc": { - "message": "Seyf vərəqindən avto-doldurma və axtarış etmə artıq daha asan və intuitivdir. Nəzər salın!" - }, "accountActions": { "message": "Hesab fəaliyyətləri" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Bu elementə düzəliş etmə icazəniz yoxdur" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Əvvəlcə PIN və ya parol ilə kilid açma tələb olunduğu üçün biometrik ilə kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrik kilid açma indi əlçatmazdır." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Bitwarden masaüstü tətbiqi bağlı olduğu üçün biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Bitwarden masaüstü tətbiqində $EMAIL$ üçün fəal olmadığına görə biometrik kilid açma əlçatmazdır.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Bilinməyən bilinməyən bir səbəbə görə biometrik kilid açma əlçatmazdır." + }, "authenticating": { "message": "Kimlik doğrulama" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra enli" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfən masaüstü tətbiqinizi güncəlləyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Biometrik kilid açmanı istifadə etmək üçün lütfən masaüstü tətbiqinizi güncəlləyin, ya da masaüstü ayarlarında barmaq izi ilə kilid açmanı sıradan çıxardın." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 46c804d27d9..8c0d35a216c 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Згенерыраваць парольную фразу" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Паўторна генерыраваць пароль" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Праверыць асобу" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ваша сховішча заблакіравана. Каб працягнуць, пацвердзіце сваю асобу." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Паказваць карткі на старонцы з укладкамі" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Спіс элементаў картак на старонцы з укладкамі для лёгкага аўтазапаўнення." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Паказваць пасведчанні на старонцы з укладкамі" @@ -2324,6 +2342,9 @@ "message": "Дамены", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Выключаныя дамены" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Вэб-сайт $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Памылка" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генерыраваць імя карыстальніка" }, @@ -3396,38 +3443,6 @@ "message": "Згарнуць/Разгарнуць", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Імпартаванне...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Даныя паспяхова імпартаваны!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Мянушка дамена" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 7cbbdc07be4..00f17c40267 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генериране на парола-фраза" }, + "passwordGenerated": { + "message": "Паролата е генерирана" + }, + "passphraseGenerated": { + "message": "Паролата-фраза е генерирана" + }, + "usernameGenerated": { + "message": "Потребителското име е генерирано" + }, + "emailGenerated": { + "message": "Е-пощата е генерирана" + }, "regeneratePassword": { "message": "Регенериране на паролата" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Потвърждаване на самоличността" }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "yourVaultIsLocked": { "message": "Трезорът е заключен — въведете главната си парола, за да продължите." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Питане за добавяне на елемент, ако такъв не бъде намерен в трезора. Прилага се за всички регистрации, в които сте вписан(а)." }, - "showCardsInVaultView": { - "message": "Показване на картите като предложения за авт. попълване в изгледа на трезора" + "showCardsInVaultViewV2": { + "message": "Картите да се показват винаги като предложения за авт. попълване в изгледа на трезора" }, "showCardsCurrentTab": { "message": "Показване на карти в страницата с разделите" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Показване на картите в страницата с разделите, за лесно автоматично попълване." }, - "showIdentitiesInVaultView": { - "message": "Показване на самоличности като предложения за автоматично попълване в изгледа на трезора" + "showIdentitiesInVaultViewV2": { + "message": "Самоличностите да се показват винаги като предложения за автоматично попълване в изгледа на трезора" }, "showIdentitiesCurrentTab": { "message": "Показване на самоличности в страницата с разделите" @@ -2285,7 +2303,7 @@ "message": "Потребителят е заключен или отписан" }, "biometricsNotUnlockedDesc": { - "message": "Отключете потребителя в настолното приложение и опитайте отново." + "message": "Отключете потребителя в самостоятелното приложение и опитайте отново." }, "biometricsNotAvailableTitle": { "message": "Отключването чрез биометрични данни не е налично" @@ -2324,6 +2342,9 @@ "message": "Домейни", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Блокирани домейни" + }, "excludedDomains": { "message": "Изключени домейни" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Битуорден няма да пита дали да запазва данните за вход в тези сайтове за всички регистрации, в които сте вписан(а). За да влезе правилото в сила, презаредете страницата." }, + "blockedDomainsDesc": { + "message": "Автоматичното попълване и други свързани функции няма да бъдат предлагани за тези уеб сайтове. Трябва да презаредите страницата, за да влязат в сила промените." + }, + "autofillBlockedNoticeV2": { + "message": "Автоматичното попълване е блокирано за този уеб сайт." + }, + "autofillBlockedNoticeGuidance": { + "message": "Променете това в настройките" + }, "websiteItemLabel": { "message": "Уеб сайт $number$ (адрес)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Промените на блокираните домейни са запазени" + }, "excludedDomainsSavedSuccess": { "message": "Промените на изключените домейни са запазени" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Грешка" }, + "decryptionError": { + "message": "Грешка при дешифриране" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Битоурден не може да дешифрира елементите от трезора посочени по-долу." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Свържете се с поддръжката", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "за да избегнете загубата на данни.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генериране на потр. име" }, @@ -3396,38 +3443,6 @@ "message": "Превключване на свиването", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Внасяне на данните Ви в Битуорден?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Защитаване на данните Ви от LastPass и внасяне в Битуорден?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Запазване като нешифрован файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Внасяне в Битуорден", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Внасяне…", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Данните бяха внесени успешно!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Грешка при внасянето. Вижте конзолата за подробности.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "По време на внасянето възникна мрежова грешка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдонимен домейн" }, @@ -3979,7 +3994,10 @@ "message": "Секретният ключ е премахнат" }, "autofillSuggestions": { - "message": "Автоматично попълване на предложения" + "message": "Предложения за авт. попълване" + }, + "itemSuggestions": { + "message": "Препоръчани елементи" }, "autofillSuggestionsTip": { "message": "Запазване на елемент за вписване за този уеб сайт, за авт. попълване" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Текстови изпращания" }, - "bitwardenNewLook": { - "message": "Биуорден има нов облик!" - }, - "bitwardenNewLookDesc": { - "message": "Сега е по-лесно и интуитивно от всякога да използвате автоматичното попълване и да търсите в раздела на трезора. Разгледайте!" - }, "accountActions": { "message": "Действия по регистрацията" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Нямате право за редактиране на този елемент" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Отключването с биометрични данни не е налично, тъй като първо се изисква отключване чрез ПИН или парола." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Отключването с биометрични данни не е налично в момента." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Отключването с биометрични данни не е налично, тъй като приложението на Биуорден за компютър не работи." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Отключването с биометрични данни не е налично, тъй като не е включено за $EMAIL$ в приложението на Битуорден за компютър.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Отключването с биометрични данни не е налично по неизвестна причина." + }, "authenticating": { "message": "Удостоверяване" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Много широко" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Моля, обновете самостоятелното приложение" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "За да използвате отключването чрез биометрични данни, обновете самостоятелното приложение или изключете отключването чрез пръстов отпечатък в настройките му." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 2da7b40554c..3428bf7bf72 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "পাসওয়ার্ড পুনঃতৈরি করুন" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "আপনার ভল্ট লক করা আছে। চালিয়ে যেতে আপনার মূল পাসওয়ার্ডটি যাচাই করান।" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 9f5a610c351..ef1565a387c 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index b38783abc82..66611001a57 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -20,16 +20,16 @@ "message": "Crea un compte" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou a Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Inicieu sessió amb la clau de pas" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Inici de sessió únic" }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvingut/da de nou" }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" @@ -120,7 +120,7 @@ "message": "Copia contrasenya" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copia frase de pas" }, "copyNote": { "message": "Copia nota" @@ -153,13 +153,13 @@ "message": "Copia el número de llicència" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Copia la clau privada" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Copieu la clau pública" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Copia l'empremta digital" }, "copyCustomField": { "message": "Copia $FIELD$", @@ -177,7 +177,7 @@ "message": "Copia notes" }, "fill": { - "message": "Fill", + "message": "Emplena", "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": "Emplena automàticament l'identitat" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Emplena el codi de verificació" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Emplena el codi de verificació", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -443,7 +443,19 @@ "message": "Genera contrasenya" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera frase de pas" + }, + "passwordGenerated": { + "message": "Contrasenya generada" + }, + "passphraseGenerated": { + "message": "Frase de pas generada" + }, + "usernameGenerated": { + "message": "Nom d'usuari generat" + }, + "emailGenerated": { + "message": "Correu electrònic generat" }, "regeneratePassword": { "message": "Regenera contrasenya" @@ -576,7 +588,7 @@ "message": "Notes" }, "privateNote": { - "message": "Private note" + "message": "Nota privada" }, "note": { "message": "Nota" @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifica identitat" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continua l'inici de sessió" + }, "yourVaultIsLocked": { "message": "La caixa forta està bloquejada. Comproveu la contrasenya mestra per continuar." }, @@ -682,7 +700,7 @@ "message": "Temps d'espera de la caixa forta" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Temps d'espera" }, "lockNow": { "message": "Bloqueja ara" @@ -782,7 +800,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Heu iniciat sessió!" }, "youSuccessfullyLoggedIn": { "message": "Heu iniciat sessió correctament" @@ -852,19 +870,19 @@ "message": "Inicia sessió" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Inicia sessió a Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "Reinicia el registre" }, "expiredLink": { - "message": "Expired link" + "message": "Enllaç caducat" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "És possible que ja tingueu un compte" }, "logOutConfirmation": { "message": "Segur que voleu tancar la sessió?" @@ -888,10 +906,10 @@ "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Fes que el vostre compte siga més segur configurant l'inici de sessió en dos passos a l'aplicació web de Bitwarden." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Continua cap a l'aplicació web?" }, "editedFolder": { "message": "Carpeta guardada" @@ -978,7 +996,7 @@ "message": "Demana d'afegir els inicis de sessió" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "Opcions de guardar a la caixa forta" }, "addLoginNotificationDesc": { "message": "La \"Notificació per afegir inicis de sessió\" demana automàticament que guardeu els nous inicis de sessió a la vostra caixa forta quan inicieu la sessió per primera vegada." @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Demana afegir un element si no se'n troba cap a la caixa forta. S'aplica a tots els comptes connectats." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Mostra sempre les targetes com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showCardsCurrentTab": { "message": "Mostra les targetes a la pàgina de pestanya" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Llista els elements de la targeta a la pàgina de pestanya per facilitar l'autoemplenat." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre les identitats com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showIdentitiesCurrentTab": { "message": "Mostra les identitats a la pàgina de pestanya" @@ -1005,7 +1023,7 @@ "message": "Llista els elements d'identitat de la pestanya de la pàgina per facilitar l'autoemplenat." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Feu clic als elements per emplenar automàticament a la vista de la caixa forta" }, "clearClipboard": { "message": "Buida el porta-retalls", @@ -1098,7 +1116,7 @@ "message": "Format de fitxer" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Aquesta exportació de fitxers estarà protegida amb contrasenya i requerirà la contrasenya del fitxer per desxifrar-la." }, "filePassword": { "message": "Contrasenya del fitxer" @@ -1122,11 +1140,11 @@ "message": "\"Contrasenya del fitxer\" i \"Confirma contrasenya del fitxer\" no coincideixen." }, "warning": { - "message": "ADVERTIMENT", + "message": "AVÍS", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advertència", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1148,7 +1166,7 @@ "message": "Compartit" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Bitwarden per empreses us permet compartir els elements de la vostra caixa forta amb altres usuaris mitjançant una organització. Més informació al lloc web bitwarden.com." }, "moveToOrganization": { "message": "Desplaça a l'organització" @@ -1206,7 +1224,7 @@ "message": "Fitxer" }, "fileToShare": { - "message": "File to share" + "message": "Fitxer per compartir" }, "selectFile": { "message": "Seleccioneu un fitxer" @@ -1242,7 +1260,7 @@ "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Accés d’emergència." }, "premiumSignUpTwoStepOptions": { "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." @@ -1287,7 +1305,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "Tot per només per $PRICE$ a l'any!", "placeholders": { "price": { "content": "$1", @@ -1317,7 +1335,7 @@ "message": "Introduïu el codi de verificació de 6 dígits de l'aplicació autenticadora." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Temps d'espera d'autenticació" }, "authenticationSessionTimedOut": { "message": "The authentication session timed out. Please restart the login process." @@ -1390,7 +1408,7 @@ "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Clau de seguretat OTP de Yubico" }, "yubiKeyDesc": { "message": "Utilitzeu una YubiKey per accedir al vostre compte. Funciona amb els dispositius YubiKey 4, 4 Nano, 4C i NEO." @@ -1413,7 +1431,7 @@ "message": "Correu electrònic" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Introduïu el codi que us hem enviat al correu electrònic." }, "selfHostedEnvironment": { "message": "Entorn d'allotjament propi" @@ -1425,7 +1443,7 @@ "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Per a la configuració avançada, podeu especificar l'URL base de cada servei de manera independent." }, "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." @@ -1440,7 +1458,7 @@ "message": "URL del servidor" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL del servidor autoallotjat", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1466,10 +1484,10 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Mostra suggeriments d'emplenament automàtic als camps del formulari" }, "showInlineMenuIdentitiesLabel": { "message": "Display identities as suggestions" @@ -1478,10 +1496,10 @@ "message": "Display cards as suggestions" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Mostra suggeriments quan la icona està seleccionada" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "S'aplica a tots els comptes connectats." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desactiveu la configuració integrada del gestor de contrasenyes del vostre navegador per evitar conflictes." @@ -1502,7 +1520,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Emplenament automàtic a la càrrega de la pàgina" }, "enableAutoFillOnPageLoad": { "message": "Habilita l'emplenament automàtic en carregar la pàgina" @@ -1514,7 +1532,7 @@ "message": "Els llocs web compromesos o no fiables poden aprofitar-se de l'emplenament automàtic en carregar de la pàgina." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Més informació sobre riscs" }, "learnMoreAboutAutofill": { "message": "Obteniu més informació sobre l'emplenament automàtic" @@ -1544,13 +1562,13 @@ "message": "Obri la caixa forta a la barra lateral" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Emplenament automàtic amb l'últim inici de sessió utilitzat per al lloc web actual" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Emplenament automàtic amb l'última targeta utilitzada per al lloc web actual" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Emplenament automàtic amb l'última identitat utilitzada per al lloc web actual" }, "commandGeneratePasswordDesc": { "message": "Genera i copia una nova contrasenya aleatòria al porta-retalls." @@ -1768,10 +1786,10 @@ "message": "Identitat" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nou $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1780,7 +1798,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Edita $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1789,7 +1807,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Mostra $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1801,13 +1819,13 @@ "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "back": { "message": "Arrere" @@ -1816,7 +1834,7 @@ "message": "Col·leccions" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ col·leccions", "placeholders": { "count": { "content": "$1", @@ -1846,7 +1864,7 @@ "message": "Notes segures" }, "sshKeys": { - "message": "SSH Keys" + "message": "Claus SSH" }, "clear": { "message": "Esborra", @@ -1872,7 +1890,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Domini base (recomanat)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1926,10 +1944,10 @@ "message": "No hi ha cap contrasenya a llistar." }, "clearHistory": { - "message": "Clear history" + "message": "Neteja l'historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Res a mostrar" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1993,10 +2011,10 @@ "message": "Desbloqueja amb codi PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Estableix el PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Estableix el PIN" }, "setYourPinCode": { "message": "Configureu el vostre codi PIN per desbloquejar Bitwarden. La configuració del PIN es restablirà si tanqueu la sessió definitivament." @@ -2017,7 +2035,7 @@ "message": "Desbloqueja amb biomètrica" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Desbloqueja amb contrasenya mestra" }, "awaitDesktop": { "message": "S’espera confirmació des de l’escriptori" @@ -2029,7 +2047,7 @@ "message": "Bloqueja amb la contrasenya mestra en reiniciar el navegador" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Sol·licita la contrasenya mestra en reiniciar el navegador" }, "selectOneCollection": { "message": "Heu d'escollir com a mínim una col·lecció." @@ -2041,13 +2059,13 @@ "message": "Clona" }, "passwordGenerator": { - "message": "Password generator" + "message": "Generador de contrasenyes" }, "usernameGenerator": { - "message": "Username generator" + "message": "Generador de nom d'usuari" }, "useThisPassword": { - "message": "Use this password" + "message": "Utilitzeu aquesta contrasenya" }, "useThisUsername": { "message": "Use this username" @@ -2096,7 +2114,7 @@ "message": "Element restaurat" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Ja teniu un compte?" }, "vaultTimeoutLogOutConfirmation": { "message": "En tancar la sessió s'eliminarà tot l'accés a la vostra caixa forta i es requerirà una autenticació en línia després del període de temps d'espera. Esteu segur que voleu utilitzar aquesta configuració?" @@ -2108,7 +2126,7 @@ "message": "Ompli automàticament i guarda" }, "fillAndSave": { - "message": "Fill and save" + "message": "Ompli i guarda" }, "autoFillSuccessAndSavedUri": { "message": "Element emplenat automàticament i URI guardat" @@ -2195,10 +2213,10 @@ "message": "Anul·la subscripció" }, "atAnyTime": { - "message": "at any time." + "message": "en qualsevol moment." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "En continuar, acceptes el" }, "and": { "message": "i" @@ -2324,6 +2342,9 @@ "message": "Dominis", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Dominis bloquejats" + }, "excludedDomains": { "message": "Dominis exclosos" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no demanarà que es guarden les dades d'inici de sessió d'aquests dominis per a tots els comptes iniciats. Heu d'actualitzar la pàgina perquè els canvis tinguen efecte." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Canvieu-ho a la configuració" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2376,7 +2409,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Detalls de l'enviament", + "message": "Detalls del Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2393,7 +2426,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Ocultar el text per defecte" + "message": "Amaga el text per defecte" }, "expired": { "message": "Caducat" @@ -2440,7 +2473,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": "Esteu segur que voleu suprimir permanentment aquest Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2451,7 +2484,7 @@ "message": "Data de supressió" }, "deletionDateDescV2": { - "message": "L'enviament s'esborrarà permanentment en aquesta data.", + "message": "El Send se suprimirà permanentment en aquesta data.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -2496,7 +2529,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send creat correctament!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { @@ -2589,7 +2622,7 @@ "message": "Es requereix verificació del correu electrònic" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Correu electrònic verificat" }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el correu electrònic per utilitzar aquesta característica. Podeu verificar el vostre correu electrònic a la caixa forta web." @@ -2631,7 +2664,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": "de $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2775,10 +2808,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "S'està exportant la caixa forta de l’organització" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Només s'exportarà la caixa forta de l'organització associada a $ORGANIZATION$. No s'inclouran els elements de les caixes fortes individuals ni d'altres organitzacions.", "placeholders": { "organization": { "content": "$1", @@ -2789,14 +2822,28 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Error de desxifrat" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacteu amb el servei d'atenció al client", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "per evitar la pèrdua de dades addicionals.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Genera un nom d'usuari" }, "generateEmail": { - "message": "Generate email" + "message": "Genera correu electrònic" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "El valor ha d'estar entre $MIN$ i $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2810,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Utilitzeu $RECOMMENDED$ caràcters o més per generar una contrasenya segura.", "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 +2867,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Utilitzeu $RECOMMENDED$ paraules o més per generar una frase de pas segura.", "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,7 +2908,7 @@ "message": "Genera un àlies de correu electrònic amb un servei de reenviament extern." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Domini del correu electrònic", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -3068,25 +3115,25 @@ "message": "Torna a enviar la notificació" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "notificationSentDevice": { "message": "S'ha enviat una notificació al vostre dispositiu." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "S'ha enviat una notificació al vostre dispositiu" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Assegureu-vos que la vostra caixa forta estiga desbloquejada i que la frase d'empremta digital coincidisca en l'altre dispositiu" }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Necessiteu una altra opció?" }, "loginInitiated": { "message": "S'ha iniciat la sessió" @@ -3146,22 +3193,22 @@ "message": "Configuració d'emplenament automàtic" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "Drecera d'emplenament automàtic" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Canvia la drecera" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Gestiona les dreceres" }, "autofillShortcut": { "message": "Drecera de teclat d'emplenament automàtic" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "La drecera d'inici de sessió no està configurada. Canvieu-ho a la configuració del navegador." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "La drecera d'emplenament automàtic és $COMMAND$. Gestioneu totes les dreceres a la configuració del navegador.", "placeholders": { "command": { "content": "$1", @@ -3212,19 +3259,19 @@ "message": "Es requereix un identificador SSO de l'organització." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Creant compte en" }, "checkYourEmail": { - "message": "Check your email" + "message": "Comprova el correu" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Seguiu l'enllaç del correu electrònic enviat a" }, "andContinueCreatingYourAccount": { "message": "and continue creating your account." }, "noEmail": { - "message": "No email?" + "message": "Sense correu electrònic?" }, "goBack": { "message": "Torna arrere" @@ -3273,11 +3320,11 @@ "message": "Dispositiu de confiança" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "No hi ha Sends actius", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Utilitzeu Send per compartir informació xifrada de manera segura amb qualsevol persona.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3354,10 +3401,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 camp necessita la vostra atenció." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ camps necessiten la vostra atenció.", "placeholders": { "count": { "content": "$1", @@ -3396,38 +3443,6 @@ "message": "Redueix/Amplia", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Voleu importar les vostres dades a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protegiu les vostres dades de LastPass i importeu-les a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guarda com a fitxer sense xifrar", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importa a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "S'està important...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Les dades s'han importat correctament!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "S'ha produït un error en importar. Consulteu la consola per obtenir més informació.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "S'ha trobat un error de xarxa durant la importació.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alies de domini" }, @@ -3506,7 +3521,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Inici de sessió nou", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { @@ -3714,7 +3729,7 @@ "message": "Accessing" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Connectat!" }, "passkeyNotCopied": { "message": "La clau de pas no es copiarà" @@ -3954,7 +3969,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Contrasenya guardada!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -3962,7 +3977,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Contrasenya actualitzada!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3979,22 +3994,25 @@ "message": "Clau de pas suprimida" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "La caixa forta està buida" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "No s'ha trobat cap element que coincidisca amb la cerca" }, "clearFiltersOrTryAnother": { "message": "Clear filters or try another search term" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Copia info - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4014,7 +4032,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Més opcions, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4024,7 +4042,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Més opcions - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4060,25 +4078,25 @@ "message": "Assign to collections" }, "copyEmail": { - "message": "Copy email" + "message": "Copia el correu electrònic" }, "copyPhone": { "message": "Copy phone" }, "copyAddress": { - "message": "Copy address" + "message": "Copia l'adreça" }, "adminConsole": { "message": "Consola d'administració" }, "accountSecurity": { - "message": "Account security" + "message": "Seguretat del compte" }, "notifications": { - "message": "Notifications" + "message": "Notificacions" }, "appearance": { - "message": "Appearance" + "message": "Aparença" }, "errorAssigningTargetCollection": { "message": "S'ha produït un error en assignar la col·lecció de destinació." @@ -4169,13 +4187,13 @@ "message": "Copy Successful" }, "upload": { - "message": "Upload" + "message": "Puja" }, "addAttachment": { - "message": "Add attachment" + "message": "Afegeix adjunt" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "La mida màxima del fitxer és de 500 MB" }, "deleteAttachmentName": { "message": "Delete attachment $NAME$", @@ -4208,7 +4226,7 @@ "message": "Filtres" }, "filterVault": { - "message": "Filter vault" + "message": "Filtra dades" }, "filterApplied": { "message": "One filter applied" @@ -4304,16 +4322,16 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Habilita l'emplenament automàtic en carregar la pàgina?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Targeta de crèdit caducada" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Dades de la targeta" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -4328,7 +4346,7 @@ "message": "Enable animations" }, "showAnimations": { - "message": "Show animations" + "message": "Mostra animacions" }, "addAccount": { "message": "Afig compte" @@ -4340,15 +4358,15 @@ "message": "Dades" }, "passkeys": { - "message": "Passkeys", + "message": "Claus de pas", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Contrasenyes", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Inicieu sessió amb la clau de pas", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4557,20 +4575,14 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "Mostra el nombre de suggeriments d'emplenament automàtic d'inici de sessió a la icona d'extensió" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Mostra accions de còpia ràpida a la caixa forta" }, "systemDefault": { "message": "System default" @@ -4579,16 +4591,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clau privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clau pública" }, "sshFingerprint": { "message": "Fingerprint" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipus de clau" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4603,7 +4615,7 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "Torneu-ho a provar" }, "vaultCustomTimeoutMinimum": { "message": "Minimum custom timeout is 1 minute." @@ -4636,11 +4648,38 @@ "message": "Restore" }, "deleteForever": { - "message": "Delete forever" + "message": "Suprimeix per sempre" }, "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4661,7 +4700,7 @@ "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Accent", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { @@ -4681,7 +4720,7 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Signe del dòlar", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { @@ -4697,15 +4736,15 @@ "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Asterisc", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Parèntesi esquerre", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Parèntesi dret", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { @@ -4745,7 +4784,7 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Barra Invertida", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { @@ -4753,7 +4792,7 @@ "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Punt i coma", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { @@ -4765,23 +4804,23 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Menor que", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Major que", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Coma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Punt", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Signe d'interrogació", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { @@ -4789,22 +4828,22 @@ "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Minúscules" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Majúscules" }, "generatedPassword": { - "message": "Generated password" + "message": "Contrasenya generada" }, "compactMode": { - "message": "Compact mode" + "message": "Mode compacte" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Noticia important" }, "setupTwoStepLogin": { "message": "Set up two-step login" @@ -4816,7 +4855,7 @@ "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." }, "remindMeLater": { - "message": "Remind me later" + "message": "Recorda-m'ho més tard" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4843,9 +4882,15 @@ "message": "Extension width" }, "wide": { - "message": "Wide" + "message": "Ample" }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bcc7020ffb3..b58ea149a1c 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Vygenerovat heslovou frázi" }, + "passwordGenerated": { + "message": "Heslo bylo vygenerováno" + }, + "passphraseGenerated": { + "message": "Heslová fráze byla vygenerována" + }, + "usernameGenerated": { + "message": "Uživatelské jméno bylo vygenerováno" + }, + "emailGenerated": { + "message": "E-mail byl vygenerován" + }, "regeneratePassword": { "message": "Vygenerovat jiné heslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Ověřit identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamčen. Pro pokračování musíte zadat hlavní heslo." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Požádá o přidání položky, pokud nebyla nalezena v trezoru. Platí pro všechny přihlášené účty." }, - "showCardsInVaultView": { - "message": "Zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" + "showCardsInVaultViewV2": { + "message": "Vždy zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" }, "showCardsCurrentTab": { "message": "Zobrazit platební karty na obrazovce Karta" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pro snadné vyplnění zobrazí platební karty na obrazovce Karta." }, - "showIdentitiesInVaultView": { - "message": "Zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" }, "showIdentitiesCurrentTab": { "message": "Zobrazit identity na obrazovce Karta" @@ -2324,6 +2342,9 @@ "message": "Domény", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokované domény" + }, "excludedDomains": { "message": "Vyloučené domény" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude žádat o uložení přihlašovacích údajů pro tyto domény pro všechny přihlášené účty. Aby se změny projevily, musíte stránku obnovit." }, + "blockedDomainsDesc": { + "message": "Automatické vyplňování a další související funkce nebudou pro tyto webové stránky nabízeny. Aby se změny projevily, musíte stránku aktualizovat." + }, + "autofillBlockedNoticeV2": { + "message": "Automatické vyplňování je pro tuto stránku zablokováno." + }, + "autofillBlockedNoticeGuidance": { + "message": "Změňte to v nastavení" + }, "websiteItemLabel": { "message": "Webová stránka $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Změny v zablokovaných doménách byly uloženy" + }, "excludedDomainsSavedSuccess": { "message": "Vyloučené změny domény byly uloženy" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Chyba" }, + "decryptionError": { + "message": "Chyba dešifrování" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nemohl dešifrovat níže uvedené položky v trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznickou podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "abyste zabránili ztrátě dat.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Vygenerovat uživatelské jméno" }, @@ -3396,38 +3443,6 @@ "message": "Přepnout sbalení", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importovat data do Bitwardenu?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Chránit Vaše data LastPass a importovat do Bitwardenu?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Uložit jako nešifrovaný soubor", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importovat do Bitwardenu", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importování...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data byla úspěšně importována!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Chyba při importu. Podrobnosti naleznete v konzoli.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Při importu došlo k chybě sítě.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Doména aliasu" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Návrhy automatického vyplňování" }, + "itemSuggestions": { + "message": "Navrhované položky" + }, "autofillSuggestionsTip": { "message": "Uložit přihlašovací údaje pro tuto stránku do automatického vyplňování" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Sends s texty" }, - "bitwardenNewLook": { - "message": "Bitwarden má nový vzhled!" - }, - "bitwardenNewLookDesc": { - "message": "Je snazší a intuitivnější než kdy jindy automaticky vyplňovat a vyhledávat z karty trezor. Mrkněte se!" - }, "accountActions": { "message": "Činnosti účtu" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Nemáte oprávnění upravit tuto položku" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrické odemknutí je nedostupné, protože je potřeba nejprve odemknout pomocí PIN nebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrické odemknutí je momentálně nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrické odemknutí není dostupné, protože je aplikace Bitwarden zavřena." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrické odemknutí není dostupné, protože není povoleno pro $EMAIL$ v desktopové aplikaci Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrické odemknutí je momentálně z neznámého důvodu nedostupné." + }, "authenticating": { "message": "Ověřování" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra široký" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte aplikaci pro stolní počítač" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Chcete-li použít biometrické odemknutí, aktualizujte aplikaci pro stolní počítač nebo v nastavení vypněte odemknutí otiskem prstů." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 3bc85b10fb5..dcc81081148 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -29,7 +29,7 @@ "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Croeso nôl" }, "setAStrongPassword": { "message": "Gosod cyfrinair cryf" @@ -171,7 +171,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copïo'r wefan" }, "copyNotes": { "message": "Copy notes" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ailgynhyrchu cyfrinair" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Gwirio'ch hunaniaeth" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Mae eich cell ar glo. Gwiriwch eich hunaniaeth i barhau." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Parthau wedi'u heithrio" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Gwall" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Cynhyrchu enw defnyddiwr" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Hoffech chi fewnforio'ch data i Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Yn mewnforio...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Cafodd y data ei fewnforio'n llwyddiannus!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4075,7 +4093,7 @@ "message": "Account security" }, "notifications": { - "message": "Notifications" + "message": "Hysbysiadau" }, "appearance": { "message": "Golwg" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 433cdebbb17..be857787d96 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generér adgangssætning" }, + "passwordGenerated": { + "message": "Adgangskode genereret" + }, + "passphraseGenerated": { + "message": "Adgangssætning genereret" + }, + "usernameGenerated": { + "message": "Brugernavn genereret" + }, + "emailGenerated": { + "message": "E-mail genereret" + }, "regeneratePassword": { "message": "Regenerér adgangskode" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Bekræft identitet" }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "yourVaultIsLocked": { "message": "Din boks er låst. Bekræft din identitet for at fortsætte." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Anmod om at tilføje et emne, hvis intet ikke findes i boksen. Gælder alle indloggede konti." }, - "showCardsInVaultView": { - "message": "Vis kort som Autoudfyldningsforslag ved Boks-visning" + "showCardsInVaultViewV2": { + "message": "Vis altid kort som Autoudfyldningsforslag ved Boks-visning" }, "showCardsCurrentTab": { "message": "Vis kort på fanebladet" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortemner på siden Fane for nem autoudfyldning." }, - "showIdentitiesInVaultView": { - "message": "Vis identiteter som Autoudfyldningsforslag ved Boks-visning" + "showIdentitiesInVaultViewV2": { + "message": "Vis altid identiteter som Autoudfyldningsforslag ved Boks-visning" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanebladet" @@ -2324,6 +2342,9 @@ "message": "Domæner", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokerede domæner" + }, "excludedDomains": { "message": "Ekskluderede domæner" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden vil ikke anmode om at gemme login-detaljer for disse domæner for alle indloggede konti. Siden skal opfriskes for at effektuere ændringerne." }, + "blockedDomainsDesc": { + "message": "Autofyldning og andre relaterede funktioner tilbydes ikke på disse websteder. Siden skal opdateres for at effektuere ændringerne." + }, + "autofillBlockedNoticeV2": { + "message": "Autoudfyldning blokeret for dette websted." + }, + "autofillBlockedNoticeGuidance": { + "message": "Ændr dette i Indstillinger" + }, "websiteItemLabel": { "message": "Websted $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blokeret domæne-ændringer gemt" + }, "excludedDomainsSavedSuccess": { "message": "Ekskluderet domæne-ændringer gemt" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Fejl" }, + "decryptionError": { + "message": "Dekrypteringsfejl" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kunne ikke dekryptere boks-emne(r) anført nedenfor." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontakt kundeservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "for at undgå yderligere tab af data.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generér brugernavn" }, @@ -3396,38 +3443,6 @@ "message": "Fold sammen/ud", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importér data til Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Beskyt LastPass-data og importér dem til Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Gem som ukrypteret fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importér til Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerer...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data er nu importeret!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fejl under import. Tjek konsollen for detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netværksfejl opstod under import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomæne" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autoudfyldningsforslag" }, + "itemSuggestions": { + "message": "Foreslåede emner" + }, "autofillSuggestionsTip": { "message": "Gem et loginemne for dette websted til autoudfyldning" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Tekst-Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden har fået et nyt look!" - }, - "bitwardenNewLookDesc": { - "message": "Det er lettere og mere intuitivt end nogensinde at autoudfylde og søge via fanen Boks. Tag et kig omkring!" - }, "accountActions": { "message": "Kontohandlinger" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Ingen tilladelse til at redigere dette emne" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisk oplåsning er utilgængelig, da PIN- eller adgangskode kræves først." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisk oplåsning er p.t. utilgængelig." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisk oplåsning er utilgængelig, da Bitwarden-appen er lukket." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisk oplåsning er utilgængelig, da det ikke er aktiveret for $EMAIL$ i Bitwarden computer-appen.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisk oplåsning er p.t. utilgængelig grundet en ukendt årsag." + }, "authenticating": { "message": "Godkender" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Opdatér venligst computerapplikationen" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "For brug af biometrisk oplåsning skal computerapplikationen opdateres eller fingeraftryksoplåsning deaktiveres i computerindstillingerne." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 2fc5297205a..6d00761b9f0 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Passphrase generieren" }, + "passwordGenerated": { + "message": "Passwort generiert" + }, + "passphraseGenerated": { + "message": "Passphrase generiert" + }, + "usernameGenerated": { + "message": "Benutzername generiert" + }, + "emailGenerated": { + "message": "E-Mail-Adresse generiert" + }, "regeneratePassword": { "message": "Passwort neu generieren" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identität verifizieren" }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "yourVaultIsLocked": { "message": "Dein Tresor ist gesperrt. Verifiziere deine Identität, um fortzufahren." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Nach dem Hinzufügen eines Eintrags fragen, wenn er nicht in deinem Tresor gefunden wurde. Gilt für alle angemeldeten Konten." }, - "showCardsInVaultView": { - "message": "Karten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + "showCardsInVaultViewV2": { + "message": "Karten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showCardsCurrentTab": { "message": "Karten auf Tab Seite anzeigen" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Karten-Einträge auf der Tab Seite anzeigen, um das Auto-Ausfüllen zu vereinfachen." }, - "showIdentitiesInVaultView": { - "message": "Identitäten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + "showIdentitiesInVaultViewV2": { + "message": "Identitäten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showIdentitiesCurrentTab": { "message": "Identitäten auf Tab Seite anzeigen" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Gesperrte Domains" + }, "excludedDomains": { "message": "Ausgeschlossene Domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden wird für alle angemeldeten Konten nicht danach fragen Zugangsdaten für diese Domains speichern. Du musst die Seite neu laden, damit die Änderungen wirksam werden." }, + "blockedDomainsDesc": { + "message": "Automatisches Ausfüllen und andere zugehörige Funktionen werden für diese Webseiten nicht angeboten. Du musst die Seite neu laden, damit die Änderungen wirksam werden." + }, + "autofillBlockedNoticeV2": { + "message": "Automatisches Ausfüllen ist für diese Website gesperrt." + }, + "autofillBlockedNoticeGuidance": { + "message": "Dies in den Einstellungen ändern" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Änderungen gesperrter Domains gespeichert" + }, "excludedDomainsSavedSuccess": { "message": "Änderungen der ausgeschlossenen Domain gespeichert" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Fehler" }, + "decryptionError": { + "message": "Entschlüsselungsfehler" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktiere unser Customer Success Team", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": ", um zusätzlichen Datenverlust zu vermeiden.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Benutzername generieren" }, @@ -3396,38 +3443,6 @@ "message": "Ein-/ausklappen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Deine Daten in Bitwarden importieren?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Deine LastPass-Daten schützen und in Bitwarden importieren?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Als unverschlüsselte Datei speichern", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "In Bitwarden importieren", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Wird importiert...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Daten erfolgreich importiert!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fehler beim Importieren. Überprüfe die Konsole für Details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netzwerkfehler beim Importieren.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias-Domain" }, @@ -3981,8 +3996,11 @@ "autofillSuggestions": { "message": "Vorschläge zum Auto-Ausfüllen" }, + "itemSuggestions": { + "message": "Vorgeschlagene Einträge" + }, "autofillSuggestionsTip": { - "message": "Speichere einen Login-Eintrag für diese Seite zum automatischen Ausfüllen" + "message": "Speichere einen Zugangsdaten-Eintrag für diese Seite zum automatischen Ausfüllen" }, "yourVaultIsEmpty": { "message": "Dein Tresor hat keine Einträge" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text-Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden hat einen neuen Look!" - }, - "bitwardenNewLookDesc": { - "message": "Auto-Ausfüllen und Suchen vom Tresor-Tab ist einfacher und intuitiver als je zuvor. Schau dich um!" - }, "accountActions": { "message": "Konto-Aktionen" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Du bist nicht berechtigt, diesen Eintrag zu bearbeiten" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da zuerst mit PIN oder Passwort entsperrt werden muss." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisches Entsperren ist derzeit nicht verfügbar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da die Bitwarden Desktop-App geschlossen ist." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da es für $EMAIL$ in der Bitwarden Desktop-App nicht aktiviert ist.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisches Entsperren ist derzeit aus einem unbekannten Grund nicht verfügbar." + }, "authenticating": { "message": "Authentifizierung" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra breit" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Bitte aktualisiere deine Desktop-Anwendung" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Um biometrisches Entsperren zu verwenden, aktualisiere bitte deine Desktop-Anwendung oder deaktiviere die Entsperrung per Fingerabdruck in den Desktop-Einstellungen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index a33bc4d0e20..fbdd2f00a1a 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Δημιουργία φράσης πρόσβασης" }, + "passwordGenerated": { + "message": "Ο κωδικός δημιουργήθηκε" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Επαναδημιουργία κωδικού πρόσβασης" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Επιβεβαίωση ταυτότητας" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε τον κύριο κωδικό πρόσβασης για να συνεχίσετε." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ζητήστε να προσθέσετε ένα αντικείμενο αν δε βρεθεί στο θησαυ/κιό σας. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, - "showCardsInVaultView": { - "message": "Εμφάνιση καρτών ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Εμφάνιση καρτών στη σελίδα Καρτέλας" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Εμφάνισε τα αντικείμενα κάρτες στη σελίδα Καρτέλα για εύκολη αυτόματη συμπλήρωση." }, - "showIdentitiesInVaultView": { - "message": "Εμφάνιση ταυτοτήτων ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Εμφάνιση ταυτοτήτων στη σελίδα καρτέλας" @@ -1005,7 +1023,7 @@ "message": "Λίστα στοιχείων ταυτότητας στη σελίδα Καρτέλας για εύκολη αυτόματη συμπλήρωση." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Κάντε κλικ στα αντικείμενα για αυτόματη συμπλήρωση στην προβολή Θησαυ/κίου" }, "clearClipboard": { "message": "Εκκαθάριση Πρόχειρου", @@ -2324,6 +2342,9 @@ "message": "Τομείς", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Αποκλεισμένοι τομείς" + }, "excludedDomains": { "message": "Εξαιρούμενοι Τομείς" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Το Bitwarden δε θα ρωτήσει για να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς, για όλους τους συνδεδεμένους λογαριασμούς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, + "blockedDomainsDesc": { + "message": "Η αυτόματη συμπλήρωση και άλλες σχετικές λειτουργίες δεν θα προσφερθούν για αυτούς τους ιστότοπους. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Ιστοσελίδα $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Οι αλλαγές αποκλεισμένων τομέων αποθηκεύτηκαν" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Σφάλμα" }, + "decryptionError": { + "message": "Σφάλμα αποκρυπτογράφησης" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Το Bitwarden δεν μπόρεσε να αποκρυπτογραφήσει τα αντικείμενα θησαυ/κίου που αναφέρονται παρακάτω." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Δημιουργία ονόματος χρήστη" }, @@ -3396,38 +3443,6 @@ "message": "Εναλλαγή σύμπτυξης", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Εισαγωγή των δεδομένων σας στο Bitwarden;", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Προστατέψτε τα δεδομένα LastPass και εισαγάγετε στο Bitwarden;", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Αποθήκευση ως μη κρυπτογραφημένο αρχείο", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Εισαγωγή στο Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Εισαγωγή...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Επιτυχής εισαγωγή δεδομένων!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Σφάλμα εισαγωγής. Ελέγξτε την κονσόλα για λεπτομέρειες.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Εμφανίστηκε σφάλμα δικτύου κατά την εισαγωγή.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Ψευδώνυμο τομέα" }, @@ -3979,7 +3994,10 @@ "message": "Το κλειδί πρόσβασης αφαιρέθηκε" }, "autofillSuggestions": { - "message": "Προτάσεις αυτόματης συμπλήρωσης" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Αποθηκεύστε ένα αντικείμενο σύνδεσης για την αυτόματη συμπλήρωση αυτού του ιστοτόπου" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Send κειμένων" }, - "bitwardenNewLook": { - "message": "Το Bitwarden έχει μια νέα εμφάνιση!" - }, - "bitwardenNewLookDesc": { - "message": "Είναι πιο ευκολότερο και πιο διαισθητικό από ποτέ στην αυτόματη συμπλήρωση και αναζήτηση από την καρτέλα Θησαυ/κιο. Ρίξτε μια ματιά τριγύρω!" - }, "accountActions": { "message": "Ενέργειες λογαριασμού" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Δεν έχετε δικαίωμα να επεξεργαστείτε αυτό το αντικείμενο" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή απαιτείται πρώτα το ξεκλείδωμα με PIN ή κωδικό πρόσβασης." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο προς το παρόν." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή η εφαρμογή Bitwarden επιφάνειας εργασίας είναι κλειστή." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Ταυτοποίηση" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Εξαιρετικά φαρδύ" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 55c9ae8616b..666dea3f5b8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -2804,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3411,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3996,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4572,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4889,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 2c13f1b9259..4e8774fc2f5 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3979,7 +3994,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to auto-fill" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index b6031381d2d..421a5839467 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identifies as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded Domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate Username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3979,7 +3994,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to auto-fill" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index d26716dd909..ea47db2c26c 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerar contraseña" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidad" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pide que se agregue un elemento si no se encuentra uno en su caja fuerte. Se aplica a todas las cuentas que hayan iniciado sesión." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar las tarjetas en la pestaña" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listar los elementos de tarjetas en la página para facilitar el auto-rellenado." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Mostrar las identidades en la página" @@ -2324,6 +2342,9 @@ "message": "Dominios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Dominios excluidos" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no pedirá que se guarden los datos de acceso para estos dominios en todas las sesiones iniciadas. Debe actualizar la página para que los cambios surtan efecto." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generar nombre de usuario" }, @@ -3396,38 +3443,6 @@ "message": "Colapsar/Expandir", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "¿Quiere importar sus datos a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "¿Quiere proteger sus datos de LastPass e importarlos a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guardar como archivo no cifrado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Se importaron los datos correctamente.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Se produjo un error al importar. Revise la consola para obtener detalles.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Se produjo un error de red durante la importación.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Seudónimo del dominio" }, @@ -3979,7 +3994,10 @@ "message": "Clave de acceso eliminada" }, "autofillSuggestions": { - "message": "Autocompletar sugerencias" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Guarda un elemento de inicio de sesión para este sitio para autocompletar" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden tiene un aspecto nuevo." - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Acciones de cuenta" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "No tiene permiso de editar este elemento" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 069135ddb08..78bc92a7ec1 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Genereeri parool uuesti" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identiteedi kinnitamine" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hoidla on lukus. Jätkamiseks sisesta ülemparool." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Kuva \"Kaart\" vaates kaardiandmed" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kuvab \"Kaart\" vaates kaardiandmeid, et neid saaks kiiresti sisestada" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Kuva \"Kaart\" vaates identiteete" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Väljajäetud domeenid" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Viga" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Genereeri kasutajanimi" }, @@ -3396,38 +3443,6 @@ "message": "Peida", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Impordin andmed Bitwardenisse?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Kaitse oma LastPassi andmeid ja impordi need Bitwardenisse?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salvesta ilma krüpteeringuta failina", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Impordi Bitwardenisse", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importimine...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Andmed on edukalt imporditud!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Ilmnes viga. Vaata täpsemaid andmeid konsoolist.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Importimisel ilmnes võrgu viga.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domeen" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 47a56583c64..d3ec813c5f5 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Berrezarri pasahitza" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Zure identitatea egiaztatu" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Zure kutxa gotorra blokeatuta dago. Egiaztatu zure identitatea jarraitzeko." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Erakutsi txartelak fitxa orrian" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Erakutsi elementuen txartelak fitxa orrian, erraz auto-betetzeko." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Erakutsi identitateak fitxa orrian" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Kanporatutako domeinuak" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Akatsa" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Sortu erabiltzaile izena" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Inportatzen...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Datuak zuzen inportatu dira!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Errorea gertatu da inportatzean. Begiratu xehetasunak kontsolan.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Sareko errorea gertatu da inportatzerakoan.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 2b8a5e20da1..68b1ad74a5b 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "تولید مجدد کلمه عبور" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "تأیید هویت" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "گاوصندوق شما قفل شده است. برای ادامه هویت خود را تأیید کنید." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "نمایش کارت‌ها در صفحه برگه" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "برای پر کردن خودکار آسان، موارد کارت را در صفحه برگه فهرست کن." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "نشان دادن هویت در صفحه برگه" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "دامنه های مستثنی" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "خطا" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "ایجاد نام کاربری" }, @@ -3396,38 +3443,6 @@ "message": "دکمه بستن", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "دامنه مستعار" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 718c0631156..6283714ed21 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Luo salalause" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Luo uusi salasana" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Vahvista henkilöllisyytesi" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Holvisi on lukittu. Jatka vahvistamalla henkilöllisyytesi." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ehdota kohteen tallennusta, jos holvistasi ei vielä löydy vastaavaa kohdetta. Koskee kaikkia kirjautuneita tilejä." }, - "showCardsInVaultView": { - "message": "Näytä kortit automaattitäytön ehdotuksina Holvi-näkymässä" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Näytä kortit välilehtiosiossa" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Näytä kortit Välilehti-sivulla automaattitäytön helpottamiseksi." }, - "showIdentitiesInVaultView": { - "message": "Näytä henkilöllisyydet automaattitäytön ehdotuksina Holvi-sivulla." + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Näytä henkilöllisyydet välilehtiosiossa" @@ -2324,6 +2342,9 @@ "message": "Verkkotunnukset", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Estetyt verkkotunnukset" + }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Automaattitäyttö on estetty tällä sivustolla." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Verkkotunnus $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Rajoitettujen verkkotunnusten muutokset tallennettiin" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Virhe" }, + "decryptionError": { + "message": "Salauksen purkuvirhe" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Luo käyttäjätunnus" }, @@ -3396,38 +3443,6 @@ "message": "Laajenna tai supista", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Haluatko tuoda tietosi Bitwardeniin?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Haluatko suojata LastPass-tietosi tuomalla ne Bitwardeniin?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Tallenna salaamattomana tiedostona", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Tuo Bitwardeniin", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Tuodaan...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Tietojen tuonti onnistui.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Tuontivirhe. Näet isätietoja hallinnasta.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Verkkovirhe tuonnin aikana.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliaksen verkkotunnus" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Automaattitäytön ehdotukset" }, + "itemSuggestions": { + "message": "Ehdotetut kohteet" + }, "autofillSuggestionsTip": { "message": "Tallenna tälle sivustolle automaattisesti täytettävä kirjautumistieto." }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Teksti-Sendit" }, - "bitwardenNewLook": { - "message": "Bitwardenilla on uusi ulkoasu!" - }, - "bitwardenNewLookDesc": { - "message": "Automaattinen täyttö ja sisällön haku Holvi-välilehdeltä on nyt entistä helpompaa ja luontevampaa. Kokeile nyt!" - }, "accountActions": { "message": "Tilitoiminnot" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Sinulla ei ole oikeutta muokata tätä kohdetta" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Todennetaan" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Erittäin leveä" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index a504cbd3174..8008d0595c3 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Muling I-generate ang Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "I-verify ang pagkakakilanlan" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Naka-lock ang iyong vault. Patunayan ang iyong pagkakakilanlan upang magpatuloy." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Hilingin na magdagdag ng isang item kung ang isa ay hindi mahanap sa iyong vault. Nalalapat sa lahat ng naka-log in na account." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Ipakita ang mga card sa Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Itala ang mga item ng card sa Tab page para sa madaling auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Ipakita ang mga pagkatao sa Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Inilayo na Domain" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Mali" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Lumikha ng username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 9566f457861..e6b06bbcb9d 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Générer une phrase de passe" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Régénérer un mot de passe" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Vérifier l'identité" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Votre coffre est verrouillé. Vérifiez votre identité pour continuer." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Demande l'ajout d'un élément si celui-ci n'est pas trouvé dans votre coffre. S'applique à tous les comptes connectés." }, - "showCardsInVaultView": { - "message": "Afficher les cartes de paiement en tant que suggestions de saisie automatique dans la vue du coffre" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afficher les cartes de paiement sur la Page d'onglet" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, - "showIdentitiesInVaultView": { - "message": "Afficher les identités en tant que suggestions de saisie automatique dans la vue du coffre" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afficher les identités sur la Page d'onglet" @@ -2324,6 +2342,9 @@ "message": "Domaines", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domaines bloqués" + }, "excludedDomains": { "message": "Domaines exclus" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ne demandera pas d'enregistrer les détails de connexion pour ces domaines pour tous les comptes connectés. Vous devez actualiser la page pour que les modifications prennent effet." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Site web $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Changements de domaines exclus enregistrés" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Erreur" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Générer un nom d'utilisateur" }, @@ -3396,38 +3443,6 @@ "message": "Déplier/Replier", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importer vos données dans Bitwarden ?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protéger vos données LastPass et importer dans Bitwarden ?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Enregistrer en tant que fichier non chiffré", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importer vers Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importation en cours...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Données importées avec succès !", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erreur lors de l'importation. Consultez la console pour plus de détails.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Une erreur réseau s'est produite lors de l'importation.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domaine de l'alias" }, @@ -3979,7 +3994,10 @@ "message": "Clé d'identification (passkey) retirée" }, "autofillSuggestions": { - "message": "Suggestions de saisie automatique" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Enregistrez un élément de connexion à remplir automatiquement pour ce site" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden a un nouveau look !" - }, - "bitwardenNewLookDesc": { - "message": "Il est plus facile et plus intuitif que jamais de remplir automatiquement les champs et d'effectuer des recherches à partir de l'onglet \"Coffre\". Jetez un coup d'œil !" - }, "accountActions": { "message": "Actions du compte" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Vous n'êtes pas autorisé à modifier cet élément" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authentification" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Très large" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b3eaae64486..075fd93319e 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Xerar frase de contrasinal" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Volver xerar contrasinal" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "A túa caixa forte está bloqueada. Verifica a túa identidade para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ofrecer gardar un elemento se non se atopa na caixa forte. Aplica a tódalas sesións iniciadas." }, - "showCardsInVaultView": { - "message": "Na caixa forte, amosar tarxetas como suxestións de Autoenchido" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Amosar tarxetas na pestana" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Lista na pestana actual as tarxetas gardadas para autoenchido." }, - "showIdentitiesInVaultView": { - "message": "Na caixa forte, amosar identidades como suxestións de Autoenchido" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Amosar identidades na pestana" @@ -2324,6 +2342,9 @@ "message": "Dominios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Dominios bloqueados" + }, "excludedDomains": { "message": "Dominios excluídos" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden non ofrecerá gardar contas para estes dominios en ningunha das sesións iniciadas. Recarga a páxina para que os cambios fornezan efecto." }, + "blockedDomainsDesc": { + "message": "O autoenchido e outras funcións relacionadas non estarán dispoñibles para estas webs. Debes recargar a páxina para que os cambios teñan efecto." + }, + "autofillBlockedNoticeV2": { + "message": "O autoenchido está bloqueado para esta web." + }, + "autofillBlockedNoticeGuidance": { + "message": "Cambia isto en axustes" + }, "websiteItemLabel": { "message": "Web $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Dominios bloqueados gardados" + }, "excludedDomainsSavedSuccess": { "message": "Dominios excluídos gardados" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Erro de descifrado" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden non puido descifrar os seguintes elementos." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacto co cliente exitoso", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "para evitar a perda de datos.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Xerar nome de usuario" }, @@ -3396,38 +3443,6 @@ "message": "Colapsar/Expandir", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar os teus datos a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protexer os teus datos de LastPass e importar a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Gardar como arquivo sen cifrar", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Datos importados con éxito!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro importando. Comproba a consola para máis detalle.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Aconteceu un erro de rede durante a importación.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias do dominio" }, @@ -3979,7 +3994,10 @@ "message": "Clave de acceso eliminada" }, "autofillSuggestions": { - "message": "Suxestións de autoenchido" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Entradas suxeridas" }, "autofillSuggestionsTip": { "message": "Gardar unha credencial como suxestión para este sitio" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Textos Send" }, - "bitwardenNewLook": { - "message": "Bitwarden ten un novo look!" - }, - "bitwardenNewLookDesc": { - "message": "É máis fácil e intuitivo que nunca autoencher e buscar dende a caixa forte. Bota un ollo!" - }, "accountActions": { "message": "Accións da conta" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Non tes permiso para modificar esta entrada" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "O desbloqueo biométrico non está dispoñible porque se require o PIN ou contrasinal primeiro." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "O desbloqueo biométrico non está dispoñible." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "O desbloqueo biométrico non está dispoñible por arquivos do sistema desconfigurados." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "O desbloqueo biométrico non está dispoñible por arquivos do sistema desconfigurados." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "O desbloqueo biométrico non está dispoñible porque a aplicación de escritorio está pechada." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "O desbloqueo biométrico non está dispoñible porque non está activada para $EMAIL$ na aplicación de escritorio.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "O desbloqueo biométrico non está dispoñible por algunha razón non prevista." + }, "authenticating": { "message": "Autenticando" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Moi ancho" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 5101c11a653..abaa5620a4a 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "צור סיסמה חדשה" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "אימות זהות" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "הכספת שלך נעולה. הזן את הסיסמה הראשית שלך כדי להמשיך." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "שגיאה" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3979,7 +3994,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "הצעות למילוי אוטומטי" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 10ad502be82..362720e4ee2 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "पहचान सत्यापित करें" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "आपकी वॉल्ट लॉक हो गई है। जारी रखने के लिए अपने मास्टर पासवर्ड को सत्यापित करें।" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "टैब पेज पर कार्ड दिखाएं" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "आसान ऑटो-फिल के लिए टैब पेज पर कार्ड आइटम सूचीबद्ध करें।" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "टैब पेज पर पहचान दिखाएं" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "बहिष्कृत डोमेन" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "एरर" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "उपयोगकर्ता नाम बनाएँ" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "डोमेन उपनाम" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "बिटवार्डन का नया रूप!" - }, - "bitwardenNewLookDesc": { - "message": "वॉल्ट टैब से ऑटोफिल और सर्च करना पहले से कहीं ज़्यादा आसान और सहज है। सबकुछ ध्यान से देखें!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index c93922cb913..ae438b5add0 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generiraj frazu lozinke" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ponovno generiraj lozinku" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Potvrdi identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pitaj za dodavanje stavke ako nije pronađena u tvojem trezoru. Primjenjuje se na sve prijavljene račune." }, - "showCardsInVaultView": { - "message": "Prikaži kartice kao prijedloge za auto-ispunu u prikazu trezora" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži platne kartice" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Prikazuj platne kartice za jednostavnu auto-ispunu." }, - "showIdentitiesInVaultView": { - "message": "Prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete" @@ -2324,6 +2342,9 @@ "message": "Domene", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Izuzete domene" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden neće nuditi spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Web stranica $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Spremljene promjene izuzete domene" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Pogreška" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generiraj korisničko ime" }, @@ -3396,38 +3443,6 @@ "message": "Sažmi/Proširi", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Uvezi svoje podatke u Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Zaštititi svoje LastPass podatke i uvezi ih u Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Spremi kao nekriptiranu datoteku", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Uvezi u Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Uvoz...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Uvoz podataka u trezor uspješan!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Greška pri uvozu. Provjeri konzolu za detalje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Došlo je do mrežne greške tijekom uvoza.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domene" }, @@ -3979,7 +3994,10 @@ "message": "Pristupni ključ uklonjen" }, "autofillSuggestions": { - "message": "Prijedlozi auto-ispune" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Spremi u auto-ispunu stavku prijave za ovu stranicu" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Send tekstovi" }, - "bitwardenNewLook": { - "message": "Bitwarden ima novi izgled!" - }, - "bitwardenNewLookDesc": { - "message": "Auto-ispuna i pretraga iz kartice Trezor je lakša i intuitivnija nego ikad prije. Razgledaj!" - }, "accountActions": { "message": "Radnje na računu" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Nemaš prava za uređivanje ove stavke" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autentifikacija" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra široko" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 8b8c9722909..0f697f8e156 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Jelmondat generálás" }, + "passwordGenerated": { + "message": "A jelszó generálásra került." + }, + "passphraseGenerated": { + "message": "A jelmondat generálásra került." + }, + "usernameGenerated": { + "message": "A felhasználónév generálásra került." + }, + "emailGenerated": { + "message": "Az email generálásra került." + }, "regeneratePassword": { "message": "Jelszó újragenerálása" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Személyazonosság ellenőrzése" }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "yourVaultIsLocked": { "message": "A széf zárolásra került. A folytatáshoz meg kell adni a mesterjelszót." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Egy elem hozzáadásának kérése, ha az nem található a széfben. Minden bejelentkezett fiókra vonatkozik." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Mindig jelenítse meg a kártyákat automatikus kitöltési javaslatként a Széf nézetben" }, "showCardsCurrentTab": { "message": "Kártyák megjelenítése a Fül oldalon" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kártyaelemek listázása a Fül oldalon a könnyű automatikus kitöltéshez." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mindig jelenítse meg a személyazonosságokat automatikus kitöltési javaslatként a Széf nézetben" }, "showIdentitiesCurrentTab": { "message": "Azonosítások megjelenítése a Fül oldalon" @@ -2324,6 +2342,9 @@ "message": "Tartomány", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Letiltott tartományok" + }, "excludedDomains": { "message": "Kizárt domainek" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "A Bitwarden nem kéri a bejelentkezési adatok mentését ezeknél a tartományoknál az összes bejelentkezési fiókra vonatkozva. A változtatások életbe lépéséhez frissíteni kell az oldalt." }, + "blockedDomainsDesc": { + "message": "Az automatikus kitöltés és az egyéb kapcsolódó funkciók ezeken a webhelyeken nincsenek a kínálatban. A változtatások életbe lépéséhez frissíteni kell az oldalt." + }, + "autofillBlockedNoticeV2": { + "message": "Az automatikus kitöltés blokkolásra került ezen a webhelyen." + }, + "autofillBlockedNoticeGuidance": { + "message": "Megváltoztatás a beállításokban" + }, "websiteItemLabel": { "message": "Webhely $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "A letiltott tartomány módosítások mentésre kerültek." + }, "excludedDomainsSavedSuccess": { "message": "A kizárt tartomány módosítások mentésre kerültek." }, @@ -2789,6 +2822,20 @@ "error": { "message": "Hiba" }, + "decryptionError": { + "message": "Visszafejtési hiba" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "A Bitwarden nem tudta visszafejteni az alább felsorolt ​​széf elemeket." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Ügyfélszolgálat elérése", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "további adatvesztés elkerülése érdekében.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Felhasználónév generálása" }, @@ -3396,38 +3443,6 @@ "message": "Összezárás váltás", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Adatok importálása a Bitwardenbe?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "A LastPass adatok megvédése és importálása a Bitwardenbe?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Mentés titkosítatlan fájlként", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importálás a Bitwardenbe", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importálás...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Az adatok sikeresen importálásra kerültek.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Hiba történt az importálás során. A részletekért ellenőrizzük a konzolt.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Hálózati hiba történt az importálás során.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Áldomain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Automatikus kitöltés javaslatok" }, + "itemSuggestions": { + "message": "Javasolt elemek" + }, "autofillSuggestionsTip": { "message": "A bejelentkezési elem mentése ehhez a webhelyhez az automatikus kitöltéshez" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Szöveg küldés" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Fiókműveletek" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Nincs jogosulltság ezen elem szerkesztéséheu." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "A biometrikus feloldás nem érhető el, mert először PIN kóddal vagy jelszóval kell feloldani." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "A biometrikus feloldás jelenleg nem érhető el." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "A biometrikus feloldás nem érhető el, mert a Bitwarden asztali alkalmazás be van zárva." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "A biometrikus feloldás nem érhető el, mert nincs engedélyezve $EMAIL$ számára a Bitwarden asztali alkalmazásban.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "A biometrikus feloldás jelenleg ismeretlen okból nem érhető el." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra széles" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Frissítsük az asztali alkalmazást." + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "A biometrikus feloldás használatához frissítsük az asztali alkalmazást vagy tiltsuk le az ujjlenyomatos feloldást az asztali beállításokban." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 7b1ad51e0b7..aad20632333 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Buat frasa sandi" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Buat Ulang Kata Sandi" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifikasi Identitas Anda" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Tanyakan untuk menambah sebuah benda jika benda itu tidak ditemukan di brankas Anda. Diterapkan ke seluruh akun yang telah masuk." }, - "showCardsInVaultView": { - "message": "Tampilkan kartu sebagai saran isi otomatis pada tampilan Brankas" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Tamplikan kartu pada halaman Tab" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Buat tampilan daftar benda dari kartu pada halaman Tab untuk isi otomatis yang mudah." }, - "showIdentitiesInVaultView": { - "message": "Tampilkan identitas sebagai saran isi otomatis pada tampilan Brankas" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Tampilkan identitas pada halaman Tab" @@ -2324,6 +2342,9 @@ "message": "Domain", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domain yang Dikecualikan" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden tidak akan meminta untuk menyimpan rincian login untuk domain tersebut. Anda harus menyegarkan halaman agar perubahan diterapkan." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Situs web $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Perubahan domain yang diabaikan telah disimpan" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Galat" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Buat nama pengguna baru" }, @@ -3396,38 +3443,6 @@ "message": "Saklar lipat", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Impor data Anda ke Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Lindungi data LastPass Anda dan impor ke Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Simpan sebagai berkas yang tidak dienkripsi", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Impor ke Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Mengimpor...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data berhasil diimpor!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Gagal mengimpor. Periksa konsol untuk rinciannya.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Kesalahan jaringan ditemui ketika mengimpor.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domain alias" }, @@ -3979,7 +3994,10 @@ "message": "Kunci sandi dihapus" }, "autofillSuggestions": { - "message": "Saran isi otomatis" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Simpan benda login untuk situs ini ke isi otomatis" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra lebar" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 6490a441832..6258385e64a 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Genera passphrase" }, + "passwordGenerated": { + "message": "Parola d'accesso generata" + }, + "passphraseGenerated": { + "message": "Frase d'accesso generata" + }, + "usernameGenerated": { + "message": "Nome utente generato" + }, + "emailGenerated": { + "message": "E-mail generata" + }, "regeneratePassword": { "message": "Rigenera password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifica identità" }, + "weDontRecognizeThisDevice": { + "message": "Non riconosciamo questo dispositivo. Inserisci il codice inviato alla tua e-mail per verificare la tua identità." + }, + "continueLoggingIn": { + "message": "Continua l'accesso" + }, "yourVaultIsLocked": { "message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Chiedi di creare un nuovo elemento se non ce n'è uno nella tua cassaforte. Si applica a tutti gli account sul dispositivo." }, - "showCardsInVaultView": { - "message": "Mostra le carte come suggerimenti di riempimento automatico nella vista cassaforte" + "showCardsInVaultViewV2": { + "message": "Mostra sempre le carte come suggerimenti di riempimento automatico nella vista cassaforte" }, "showCardsCurrentTab": { "message": "Mostra le carte nella sezione Scheda" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Mostra le carte nella sezione Scheda per riempirle automaticamente." }, - "showIdentitiesInVaultView": { - "message": "Mostra le identità come suggerimenti di riempimento automatico nella vista cassaforte" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre le identità come suggerimenti di riempimento automatico nella vista cassaforte" }, "showIdentitiesCurrentTab": { "message": "Mostra le identità nella sezione Scheda" @@ -2324,6 +2342,9 @@ "message": "Domini", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domini bloccati" + }, "excludedDomains": { "message": "Domini esclusi" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden non chiederà di salvare le credenziali di accesso per questi domini per tutti gli account sul dispositivo. Ricarica la pagina affinché le modifiche abbiano effetto." }, + "blockedDomainsDesc": { + "message": "Per questi siti, l'auto-completamento e funzionalità simili non saranno disponibili. Ricarica la pagina per applicare le modifiche." + }, + "autofillBlockedNoticeV2": { + "message": "La compilazione automatica è bloccata per questo sito." + }, + "autofillBlockedNoticeGuidance": { + "message": "Modifica questo nelle impostazioni" + }, "websiteItemLabel": { "message": "Sito $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Modifiche ai domini bloccati salvate" + }, "excludedDomainsSavedSuccess": { "message": "Modifiche del dominio escluso salvate" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Errore" }, + "decryptionError": { + "message": "Errore di decifrazione" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden non può decifrare gli elementi elencati di seguito." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contatta il cliente correttamente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "per evitare ulteriori perdite di dati.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Genera nome utente" }, @@ -3396,38 +3443,6 @@ "message": "Comprimi/espandi", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importare i tuoi dati su Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteggere i tuoi dati LastPass e importarli su Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salva come file non crittografato", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importa su Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importazione in corso...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dati importati!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Errore durante l'importazione. Controlla la console per i dettagli.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Errore di connessione durante l'importazione.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Dominio alias" }, @@ -3979,7 +3994,10 @@ "message": "Passkey rimossa" }, "autofillSuggestions": { - "message": "Suggerimenti per il riempimento automatico" + "message": "Suggerimenti riempimento automatico" + }, + "itemSuggestions": { + "message": "Elementi suggeriti" }, "autofillSuggestionsTip": { "message": "Salva un elemento di accesso per questo sito da riempire automaticamente" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Send Testo" }, - "bitwardenNewLook": { - "message": "Bitwarden ha un nuovo look!" - }, - "bitwardenNewLookDesc": { - "message": "È più facile e intuitivo che mai utilizzare il riempimento automatico e cercare dalla scheda Cassaforte. Dai un'occhiata!" - }, "accountActions": { "message": "Azioni dell'account" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Non hai i permessi per modificare questo elemento" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Lo sblocco biometrico non è disponibile perché è necessario prima sbloccare con PIN o parola d'accesso." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Lo sblocco biometrico non è attualmente disponibile." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Lo sblocco biometrico non è disponibile perché l'app desktop Bitwarden è chiusa." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Lo sblocco biometrico non è disponibile perché non è abilitato per $EMAIL$ nell'app desktop Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Lo sblocco biometrico non è attualmente disponibile per un motivo sconosciuto." + }, "authenticating": { "message": "Autenticazione" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Molto larga" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aggiornare l'applicazione desktop" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Per usare lo sblocco biometrico, aggiornare l'applicazione desktop o disabilitare lo sblocco dell'impronta digitale nelle impostazioni del desktop." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 42dd73020ec..e6e464a67de 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "パスフレーズを生成" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "パスワードの再生成" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "本人確認を行う" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "保管庫がロックされています。続行するには本人確認を行ってください。" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "保管庫にアイテムが見つからない場合は、アイテムを追加するよう要求します。ログインしているすべてのアカウントに適用されます。" }, - "showCardsInVaultView": { - "message": "保管庫ビューに自動入力の候補としてカードを表示する" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "タブページにカードを表示" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "自動入力を簡単にするために、タブページにカードアイテムを表示します" }, - "showIdentitiesInVaultView": { - "message": "保管庫ビューに自動入力の候補として ID を表示する" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "タブページに ID を表示" @@ -2324,6 +2342,9 @@ "message": "ドメイン", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "除外するドメイン" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden はログインしているすべてのアカウントで、これらのドメインのログイン情報を保存するよう要求しません。 変更を有効にするにはページを更新する必要があります。" }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "ウェブサイト $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "除外ドメインの変更を保存しました" }, @@ -2789,6 +2822,20 @@ "error": { "message": "エラー" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "ユーザー名を生成" }, @@ -3396,38 +3443,6 @@ "message": "開く/閉じる", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Bitwarden にデータをインポートしますか?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass データを Bitwarden にインポートしますか?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "暗号化されていないファイルとして保存", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden にインポート", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "インポート中...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "データをインポートしました!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "インポート中にエラーが発生しました。詳細はコンソールを確認してください。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "インポート中にネットワークエラーが発生しました。", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "エイリアスドメイン" }, @@ -3979,7 +3994,10 @@ "message": "パスキーを削除しました" }, "autofillSuggestions": { - "message": "候補を自動入力する" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "自動入力するためにこのサイトのログインアイテムを保存します" @@ -4557,12 +4575,6 @@ "textSends": { "message": "テキスト Send" }, - "bitwardenNewLook": { - "message": "Bitwarden が新しい外観になりました。" - }, - "bitwardenNewLookDesc": { - "message": "保管庫タブからの自動入力と検索がこれまで以上に簡単で直感的になりました。" - }, "accountActions": { "message": "アカウントの操作" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "このアイテムを編集する権限がありません" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "認証中" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "エクストラワイド" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 5da73c6755b..795d78ca6e0 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "დომენები", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "შეცდომა" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "შემოტანა...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "ავთენტიკაცია" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 6ab3755c8f4..a228cf8ff55 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 09e26c18b5b..594c9a83760 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಪುನರುತ್ಪಾದಿಸಿ" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಲಾಕ್ ಆಗಿದೆ. ಮುಂದುವರೆಯಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "ಹೊರತುಪಡಿಸಿದ ಡೊಮೇನ್ಗಳು" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index d6da55f600d..0aeb64283b8 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "암호 생성" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "비밀번호 재생성" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "신원 확인" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, - "showCardsInVaultView": { - "message": "보관함 보기에서 카드 자동완성 제안를 표시" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, - "showIdentitiesInVaultView": { - "message": "보관함 보기에서 신원들의 자동완성 제안을 표시" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "탭 페이지에 신원들을 표시" @@ -1005,7 +1023,7 @@ "message": "간편한 자동완성을 위해 탭에 신원 항목들을 나열" }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "보관함 보기에서 항목을 클릭하여 자동 완성" }, "clearClipboard": { "message": "클립보드 비우기", @@ -1107,10 +1125,10 @@ "message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다." }, "accountRestrictedOptionDescription": { - "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으로 가져오기를 제한해보세요. " + "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으만 가져오기를 제한합니다." }, "passwordProtectedOptionDescription": { - "message": "파일 비밀번호를 설정하여, 내보내기를 암호화하고, 해독에 그 파일 비밀번호를 사용하는 Bitwarden계정에 가져오세요." + "message": "파일 비밀번호를 설정하여 내보내기를 암호화하고, 어느 Bitwarden 계정으로든 해독에 그 파일 비밀번호를 사용하여 가져오세요." }, "exportTypeHeading": { "message": "내보내기 유형" @@ -1478,7 +1496,7 @@ "message": "카드를 제안으로 표시" }, "showInlineMenuOnIconSelectionLabel": { - "message": "아이콘을 선택하면 제안이 표시됩니다." + "message": "아이콘을 선택할 때 제안을 표시" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "로그인한 모든 계정에 적용" @@ -2324,6 +2342,9 @@ "message": "도메인", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "제외된 도메인" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "BItwarden은 로그인한 모든 계정에 대해 이러한 도메인에 대한 로그인 세부 정보를 저장하도록 요청하지 않습니다. 변경 사항을 적용하려면 페이지를 새로 고쳐야 합니다" }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "웹사이트 $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "제외된 도메인 변경 사항 저장됨" }, @@ -2789,6 +2822,20 @@ "error": { "message": "오류" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "아이디 생성" }, @@ -3396,38 +3443,6 @@ "message": "토글이 붕괴됨", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "데이터를 Bitwarden으로 가져오시겠습니까?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass 데이터를 보호하고 Bitwarden으로 가져오시겠습니까?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "암호화되지 않은 파일로 저장", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden으로 가져오기", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "가져오는 중...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "데이터 가져오기 성공!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "가져오는 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "가져오기 중에 네트워크 오류가 발생했습니다.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "도메인 별칭" }, @@ -3979,7 +3994,10 @@ "message": "패스키 제거됨" }, "autofillSuggestions": { - "message": "자동 완성 제안" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "이 사이트에서 자동으로 작성할 로그인 항목 저장" @@ -4557,12 +4575,6 @@ "textSends": { "message": "텍스트 Send" }, - "bitwardenNewLook": { - "message": "Bitwarden이 새로운 모습으로 돌아왔습니다!" - }, - "bitwardenNewLookDesc": { - "message": "보관함 탭에서 자동 완성하고 검색하는 것이 그 어느 때보다 쉽고 직관적입니다. 둘러보세요!" - }, "accountActions": { "message": "계정 작업" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "아이템을 수정할 권한이 없습니다." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "인증 중" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "매우 넓게" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index eaf1cb9f9db..901fd6cea26 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Generuoti slaptažodį iš naujo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Patvirtinti tapatybę" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Jūsų saugykla užrakinta. Norėdami tęsti, patikrinkite pagrindinį slaptažodį." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Paprašykite pridėti elementą, jei jo nerasta Jūsų saugykloje. Taikoma visoms prisijungusioms paskyroms." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Visada rodyti korteles kaip automatinio pildymo pasiūlymus saugyklos rodinyje" }, "showCardsCurrentTab": { "message": "Rodyti korteles skirtuko puslapyje" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pateikti kortelių elementų skirtuko puslapyje sąrašą, kad būtų lengva automatiškai užpildyti." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Visada rodyti tapatybes kaip automatinio pildymo pasiūlymus saugyklos rodinyje" }, "showIdentitiesCurrentTab": { "message": "Rodyti tapatybes skirtuko puslapyje" @@ -2324,6 +2342,9 @@ "message": "Domenai", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Išskirti domenai" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "„Bitwarden“ neprašys išsaugoti prisijungimo detalių šiems domenams, visose prisijungusiose paskyrose. Turite atnaujinti puslapį, kad pokyčiai pradėtų galioti." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Klaida" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generuoti vartotojo vardą" }, @@ -3396,38 +3443,6 @@ "message": "Perjungti sutrumpinimą", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importuoti duomenis į Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Apsaugoti LastPass duomenis ir importuoti į Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Išsaugoti kaip neužšifruotą failą", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importuoti į Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importuojama...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Duomenys sėkmingai importuoti.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Klaida importuojant. Išsamesnės informacijos patikrink konsolėje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Importuojant įvyko tinklo klaida.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domeno slapyvardis" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 2c2a9c3c69c..822ea75915c 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Izveidot paroles vārdkopu" }, + "passwordGenerated": { + "message": "Parole izveidota" + }, + "passphraseGenerated": { + "message": "Paroles vārdkopa izveidota" + }, + "usernameGenerated": { + "message": "Lietotājvārds izveidots" + }, + "emailGenerated": { + "message": "E-pasta adrese izveidota" + }, "regeneratePassword": { "message": "Pārizveidot paroli" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identitātes apliecināšana" }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "yourVaultIsLocked": { "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Vaicāt, vai pievienot vienumu, ja glabātavā tāds nav atrodams. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, - "showCardsInVaultView": { - "message": "Rādīt kartes kā automātiskās aizpildes ieteikumus glabātavas skatā" + "showCardsInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt kartes kā automātiskās aizpildes ieteikumus" }, "showCardsCurrentTab": { "message": "Rādīt kartes cilnes lapā" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Attēlot kartes ciļņu lapā vieglākai aizpildīšanai." }, - "showIdentitiesInVaultView": { - "message": "Rādīt identitātes kā automātiskās aizpildes ieteikumus glabātavas skatā" + "showIdentitiesInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt identitātes kā automātiskās aizpildes ieteikumus" }, "showIdentitiesCurrentTab": { "message": "Rādīt identitātes cilnes pārskatā" @@ -2324,6 +2342,9 @@ "message": "Domēna vārdi", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Liegtie domēna vārdi" + }, "excludedDomains": { "message": "Izņēmuma domēni" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nevaicās saglabāt pieteikšanās datus visiem šī domēna kontiem, kuri ir pieteikušies. Ir jāpārlādē lapa, lai iedarbotos izmaiņas." }, + "blockedDomainsDesc": { + "message": "Automātiskā aizpilde un citas saistītās iespējas šajās tīmekļvietnēs netiks piedāvātas. Ir jāatsvaidzina lapa, lai izmaiņas iedarbotos." + }, + "autofillBlockedNoticeV2": { + "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta." + }, + "autofillBlockedNoticeGuidance": { + "message": "To var mainīt iestatījumos" + }, "websiteItemLabel": { "message": "Tīmekļvietne $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Liegtā domēna vārda izmaiņas sglabātas" + }, "excludedDomainsSavedSuccess": { "message": "Saglabātas vērā neņemto domēna vārdu izmaiņas" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Kļūda" }, + "decryptionError": { + "message": "Atšifrēšanas kļūda" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nevarēja atšifrēt zemāk uzskaitītos glabātavas vienumus." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Jāsazinās ar klientu atbalstu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "lai izvairītos no papildu datu zaudējumiem.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Izveidot lietotājvārdu" }, @@ -3396,38 +3443,6 @@ "message": "Pārslēgt sakļaušanu", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Ievietot datus Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Aizsargāt LastPass datus un ievietot tos Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Saglabāt kā nešifrētu datni", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Ievietot Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Ievieto...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dati veiksmīgi ievietoti.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Kļūda ievietošanā. Jāpārbauda konsole, lai iegūtu vairāk informācijas.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Ievietošanas laikā atgadījās tīkla kļūda.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aizstājdomēns" }, @@ -3979,7 +3994,10 @@ "message": "Piekļuves atslēga noņemta" }, "autofillSuggestions": { - "message": "Ieteikumi automātiskajai aizpildei" + "message": "Automātiskās aizpildes ieteikumi" + }, + "itemSuggestions": { + "message": "Ieteiktie vienumi" }, "autofillSuggestionsTip": { "message": "Saglabāt pieteikšanās vienumi, ko automātiski aizpildīt šajā vietnē" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Teksta Send" }, - "bitwardenNewLook": { - "message": "Bitwarden ir jauns izskats." - }, - "bitwardenNewLookDesc": { - "message": "Veikt automātisko aizpildi un meklēšanu glabātavas cilnē ir vienkāršāk un izprotamāk kā jebkad. Apskati izmaiņas!" - }, "accountActions": { "message": "Konta darbības" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Nav nepieciešamo atļauju, lai labotu šo vienumu" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama, jo vispirms ir nepieciešama atslēgšana ar PIN vai paroli." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Atslēgšana ar biometriju nav pieejama, jo Bitwarden darbvirsmas lietotne ir aizvērta." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Atslēgšana ar biometriju nav pieejama, jo tā nav iespējota $EMAIL$ Bitwarden darbvirsmas lietotnē.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama nezināma iemesla dēļ." + }, "authenticating": { "message": "Autentificē" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Ļoti plats" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lūgums atjaunināt darbvirsmas lietotni" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Lai izmantotu atslēgšanu ar biometriju, lūgums atjaunināt darbvirsmas lietotni vai atspējot atslēgšanu ar pirkstu nospiedumu darbvirsmas iestatījumos." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index cd210c85ce1..66da0de2b53 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "പാസ്സ്‌വേഡ് വീണ്ടും സൃഷ്ടിക്കുക" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "തങ്ങളുടെ വാൾട് പൂട്ടിയിരിക്കുന്നു. തുടരുന്നതിന് നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് പരിശോധിക്കുക." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index d156e6d6458..757bce23807 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "पासवर्ड पुनर्जनित करा" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "ओळख सत्यापित करा" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "तुमची तिजोरीला कुलूप लावले आहे. पुढे जाण्यासाठी तुमची ओळख सत्यापित करा." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 6ab3755c8f4..a228cf8ff55 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 16469051a0c..6eccbe93cc5 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -20,19 +20,19 @@ "message": "Opprett en konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Er du ny til Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Logg inn med passnøkkel" }, "useSingleSignOn": { "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Velkommen tilbake" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Velg et sterkt passord" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -81,10 +81,10 @@ "message": "Et hint for hovedpassordet (valgfritt)" }, "joinOrganization": { - "message": "Join organization" + "message": "Bli med i organisasjonen" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Bli med i $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -138,31 +138,31 @@ "message": "Kopier sikkerhetskoden" }, "copyName": { - "message": "Copy name" + "message": "Kopiér navn" }, "copyCompany": { "message": "Copy company" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiér fødselsnummer" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiér passnummer" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiér lisensnummer" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiér privat nøkkel" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiér offentlig nøkkel" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopiér fingeravtrykk" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiér $FIELD$", "placeholders": { "field": { "content": "$1", @@ -171,13 +171,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiér nettsted" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiér notater" }, "fill": { - "message": "Fill", + "message": "Fyll", "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": "Auto-utfyll identitet" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Fyll inn verifiseringskode" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Fyll inn verifiseringskode", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -239,13 +239,13 @@ "message": "Legg til en gjenstand" }, "accountEmail": { - "message": "Account email" + "message": "Kontoens E-postadresse" }, "requestHint": { - "message": "Request hint" + "message": "Be om et hint" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Be om passordhint" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -316,7 +316,7 @@ "message": "Logg ut" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Om Bitwarden" }, "about": { "message": "Om" @@ -325,7 +325,7 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Vil du fortsette til bitwarden.com?" }, "bitwardenForBusiness": { "message": "Bitwarden for Business" @@ -382,7 +382,7 @@ "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "Ingen mapper er lagt til" }, "createFoldersToOrganize": { "message": "Create folders to organize your vault items" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Omgenerer et passord" }, @@ -475,7 +487,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Inkluder store bokstaver", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -483,7 +495,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Inkluder små bokstaver", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -499,7 +511,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Inkluder spesialtegn", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -526,7 +538,7 @@ "message": "Minste antall spesialtegn" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Unngå forvekslingsbare tegn", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -564,19 +576,19 @@ "message": "Favoritt" }, "unfavorite": { - "message": "Unfavorite" + "message": "Fjern favorittstempel" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Gjenstand lagt til i favorittene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Gjenstand fjernet fra favorittene" }, "notes": { "message": "Notater" }, "privateNote": { - "message": "Private note" + "message": "Privat notat" }, "note": { "message": "Notat" @@ -597,10 +609,10 @@ "message": "Åpne" }, "launchWebsite": { - "message": "Launch website" + "message": "Åpne nettstedet" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Åpne nettstedet $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Bekreft identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette." }, @@ -657,7 +675,7 @@ "message": "Your account is locked" }, "or": { - "message": "or" + "message": "eller" }, "unlock": { "message": "Lås opp" @@ -745,7 +763,7 @@ "message": "Your master password cannot be recovered if you forget it!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Få et hint om hovedpassordet" }, "errorOccurred": { "message": "En feil har oppstått" @@ -785,10 +803,10 @@ "message": "You have been logged in!" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Du har vellykket logget inn" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Du kan lukke dette vinduet" }, "masterPassSent": { "message": "Vi har sendt deg en E-post med hintet til superpassordet." @@ -834,10 +852,10 @@ "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Lær mer om autentisering" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Kopier autentiseringsnøkkel (TOTP)" }, "loggedOut": { "message": "Logget av" @@ -852,19 +870,19 @@ "message": "Logg inn" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Logg inn på Bitwarden" }, "restartRegistration": { "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Utløpt lenke" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Du har kanskje allerede en konto" }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge av?" @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Vis kort på fanesiden" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortelementer på fanesiden for lett auto-utfylling." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanesiden" @@ -1049,7 +1067,7 @@ "message": "Lås opp" }, "additionalOptions": { - "message": "Additional options" + "message": "Ekstra innstillinger" }, "enableContextMenuItem": { "message": "Vis alternativer for kontekstmeny" @@ -1113,7 +1131,7 @@ "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "Eksporttype" }, "accountRestricted": { "message": "Account restricted" @@ -1126,7 +1144,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advarsel", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1206,7 +1224,7 @@ "message": "Fil" }, "fileToShare": { - "message": "File to share" + "message": "Filen som skal deles" }, "selectFile": { "message": "Velg en fil." @@ -1242,7 +1260,7 @@ "message": "1 GB med kryptert fillagring for filvedlegg." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Nødtilgang." }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." @@ -1413,7 +1431,7 @@ "message": "E-post" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Skriv inn koden du har fått tilsendt på E-post." }, "selfHostedEnvironment": { "message": "Selvbetjent miljø" @@ -1466,7 +1484,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Autoutfyllingsforslag" }, "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" @@ -1514,7 +1532,7 @@ "message": "Kompromitterte eller upålitelige nettsider kan utnytte auto-utfylling når du laster inn siden." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Lær mer om risikoer" }, "learnMoreAboutAutofill": { "message": "Lær mer om auto-utfylling" @@ -1768,10 +1786,10 @@ "message": "Identitet" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH-nøkkel" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Ny $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1780,7 +1798,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Rediger $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1789,7 +1807,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Vis $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1801,7 +1819,7 @@ "message": "Passordhistorikk" }, "generatorHistory": { - "message": "Generator history" + "message": "Generatorhistorikk" }, "clearGeneratorHistoryTitle": { "message": "Clear generator history" @@ -1846,7 +1864,7 @@ "message": "Sikre notiser" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH-nøkler" }, "clear": { "message": "Tøm", @@ -1929,7 +1947,7 @@ "message": "Tøm historikk" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Ingenting å vise" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -2017,7 +2035,7 @@ "message": "Lås opp med biometri" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Lås opp med hovedpassord" }, "awaitDesktop": { "message": "Venter på bekreftelse fra skrivebordsprogrammet" @@ -2029,7 +2047,7 @@ "message": "Lås med hovedpassordet når du starter nettleseren på nytt" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Krev hovedpassord ved omstart av nettleseren" }, "selectOneCollection": { "message": "Du må velge minst én samling." @@ -2041,22 +2059,22 @@ "message": "Klon" }, "passwordGenerator": { - "message": "Password generator" + "message": "Passordgenerator" }, "usernameGenerator": { - "message": "Username generator" + "message": "Brukernavngenerator" }, "useThisPassword": { "message": "Bruk dette passordet" }, "useThisUsername": { - "message": "Use this username" + "message": "Bruk dette brukernavnet" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Bruk denne generatoren", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { @@ -2096,7 +2114,7 @@ "message": "Gjenopprettet objekt" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Har du allerede en konto?" }, "vaultTimeoutLogOutConfirmation": { "message": "Hvis du logger ut, fjerner du all tilgang til hvelvet ditt og krever online godkjenning etter tidsavbrudd. Er du sikker på at du vil bruke denne innstillingen?" @@ -2108,7 +2126,7 @@ "message": "Autofyll og lagre" }, "fillAndSave": { - "message": "Fill and save" + "message": "Fyll og lagre" }, "autoFillSuccessAndSavedUri": { "message": "Autoutfylt objekt og lagret URI" @@ -2195,10 +2213,10 @@ "message": "Avslutt abonnement" }, "atAnyTime": { - "message": "at any time." + "message": "når som helst." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ved å fortsette, samtykker du til" }, "and": { "message": "og" @@ -2324,6 +2342,9 @@ "message": "Domener", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokkerte domener" + }, "excludedDomains": { "message": "Ekskluderte domener" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,11 +2381,14 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, "limitSendViews": { - "message": "Limit views" + "message": "Begrens visninger" }, "limitSendViewsHint": { "message": "No one can view this Send after the limit is reached.", @@ -2383,7 +2416,7 @@ "message": "Tekst" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Teksten som skal deles" }, "sendTypeFile": { "message": "Fil" @@ -2619,7 +2652,7 @@ "message": "Velg mappe …" }, "noFoldersFound": { - "message": "No folders found", + "message": "Ingen mapper ble funnet", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2631,7 +2664,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": "av $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2789,6 +2822,20 @@ "error": { "message": "Feil" }, + "decryptionError": { + "message": "Dekrypteringsfeil" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generer brukernavn" }, @@ -2796,7 +2843,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Verdien må være mellom $MIN$ og $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2861,7 +2908,7 @@ "message": "Generer et e-postalias med en ekstern videresendingstjeneste." }, "forwarderDomainName": { - "message": "Email domain", + "message": "E-postdomene", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -2883,11 +2930,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Nettsted: $WEBSITE$. Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2931,7 +2978,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Ugyldig $SERVICENAME$-domene.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3068,16 +3115,16 @@ "message": "Send varslingen på nytt" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "notificationSentDevice": { "message": "Et varsel er sendt til enheten din." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Et varsel ble sendt til enheten din" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" @@ -3086,7 +3133,7 @@ "message": "You will be notified once the request is approved" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Trenger du et annet alternativ?" }, "loginInitiated": { "message": "Login initiated" @@ -3188,7 +3235,7 @@ "message": "Device approval required. Select an approval option below:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Enhetsgodkjennelse kreves" }, "selectAnApprovalOptionBelow": { "message": "Select an approval option below" @@ -3212,7 +3259,7 @@ "message": "Organization SSO identifier is required." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Oppretter en konto på" }, "checkYourEmail": { "message": "Check your email" @@ -3230,7 +3277,7 @@ "message": "Gå tilbake" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "for å redigere E-postadressen din." }, "eu": { "message": "EU", @@ -3258,7 +3305,7 @@ "message": "Du vil bli varslet når det er godkjent." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Har du problemer med å logge inn?" }, "loginApproved": { "message": "Innlogging godkjent" @@ -3270,7 +3317,7 @@ "message": "Active user email not found. Logging you out." }, "deviceTrusted": { - "message": "Device trusted" + "message": "Enheten er betrodd" }, "sendsNoItemsTitle": { "message": "No active Sends", @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Vil du importere dataene dine til Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Lagre som ukryptert fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importer til Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerer …", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dataene ble vellykket importert!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias-domene" }, @@ -3458,7 +3473,7 @@ "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Bitwardens autoutfyllingsmeny", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { @@ -3514,7 +3529,7 @@ "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "Nytt kort", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { @@ -3544,7 +3559,7 @@ "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Importeringsfeil" }, "importErrorDesc": { "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." @@ -3708,16 +3723,16 @@ "message": "Vault data exported" }, "typePasskey": { - "message": "Passkey" + "message": "Passnøkkel" }, "accessing": { "message": "Accessing" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Innlogget!" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Passkoden vil ikke bli kopiert" }, "passkeyNotCopiedAlert": { "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" @@ -3747,10 +3762,10 @@ "message": "Bekreft" }, "savePasskey": { - "message": "Save passkey" + "message": "Lagre passnøkkel" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Lagre passnøkkelen som en ny pålogging" }, "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" @@ -3813,7 +3828,7 @@ "message": "Approve the login request in your authentication app or enter a one-time passcode." }, "passcode": { - "message": "Passcode" + "message": "Passkode" }, "lastPassMasterPassword": { "message": "LastPass-hovedpassord" @@ -3853,7 +3868,7 @@ "message": "Bytt kontoer" }, "switchToAccount": { - "message": "Switch to account" + "message": "Bytt til konto" }, "activeAccount": { "message": "Aktiv konto" @@ -3874,13 +3889,13 @@ "message": "låst opp" }, "server": { - "message": "server" + "message": "tjener" }, "hostedAt": { "message": "betjent hos" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Bruk enhets- eller maskinvarenøkkel" }, "justOnce": { "message": "Kun én gang" @@ -3902,7 +3917,7 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Vil du fortsette til nettleserinnstillingene?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { @@ -3946,7 +3961,7 @@ "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Gjør det til standarden", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -3954,7 +3969,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Passordet ble lagret!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -3962,7 +3977,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Passordet ble oppdatert!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3973,7 +3988,7 @@ "message": "Suksess" }, "removePasskey": { - "message": "Remove passkey" + "message": "Fjern passordnøkkel" }, "passkeyRemoved": { "message": "Passkey removed" @@ -3981,11 +3996,14 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Hvelvet ditt er tomt" }, "noItemsMatchSearch": { "message": "No items match your search" @@ -4014,7 +4032,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Flere innstillinger, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4024,7 +4042,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Flere innstillinger - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4034,7 +4052,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Vis gjenstand - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4044,7 +4062,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "Autoutfyll - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4057,7 +4075,7 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Legg til i samlinger" }, "copyEmail": { "message": "Copy email" @@ -4087,7 +4105,7 @@ "message": "Error assigning target folder." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Vis gjenstander i $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4097,7 +4115,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Tilbake til $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4110,7 +4128,7 @@ "message": "Ny" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Fjern $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4120,13 +4138,13 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Gjenstander uten mappe" }, "itemDetails": { - "message": "Item details" + "message": "Gjenstandens detaljer" }, "itemName": { - "message": "Item name" + "message": "Gjenstandens navn" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -4154,10 +4172,10 @@ "message": "Tilleggsinformasjon" }, "itemHistory": { - "message": "Item history" + "message": "Gjenstandshistorikk" }, "lastEdited": { - "message": "Last edited" + "message": "Nyligst redigert" }, "ownerYou": { "message": "Owner: You" @@ -4172,10 +4190,10 @@ "message": "Last opp" }, "addAttachment": { - "message": "Add attachment" + "message": "Legg til vedlegg" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Maksimal filstørrelse er 500 MB" }, "deleteAttachmentName": { "message": "Delete attachment $NAME$", @@ -4187,7 +4205,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Last ned $NAME$", "placeholders": { "name": { "content": "$1", @@ -4211,10 +4229,10 @@ "message": "Filter vault" }, "filterApplied": { - "message": "One filter applied" + "message": "Ett filter er benyttet" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ filtre er benyttet", "placeholders": { "count": { "content": "$1", @@ -4223,16 +4241,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "Personlige detaljer" }, "identification": { - "message": "Identification" + "message": "Identifikasjon" }, "contactInfo": { "message": "Kontaktinfo" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "Last ned - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4241,17 +4259,17 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "kortnummeret slutter med", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { "message": "Legitimasjoner for innlogging" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Autentiseringsnøkkel" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autoutfyllings-innstillinger" }, "websiteUri": { "message": "Website (URI)" @@ -4267,7 +4285,7 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Nettsted lagt til" }, "addWebsite": { "message": "Legg til nettsted" @@ -4276,7 +4294,7 @@ "message": "Slett nettsted" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Standard ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4307,16 +4325,16 @@ "message": "Autofill on page load?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Utløpt kort" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Kortdetaljer" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$-detaljer", "placeholders": { "brand": { "content": "$1", @@ -4328,7 +4346,7 @@ "message": "Aktiver animasjoner" }, "showAnimations": { - "message": "Show animations" + "message": "Vis animasjoner" }, "addAccount": { "message": "Legg til konto" @@ -4340,15 +4358,15 @@ "message": "Data" }, "passkeys": { - "message": "Passkeys", + "message": "Passnøkler", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Passord", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Logg inn med passnøkkel", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4373,16 +4391,16 @@ } }, "addField": { - "message": "Add field" + "message": "Legg til felt" }, "add": { "message": "Legg til" }, "fieldType": { - "message": "Field type" + "message": "Felttype" }, "fieldLabel": { - "message": "Field label" + "message": "Feltetikett" }, "textHelpText": { "message": "Use text fields for data like security questions" @@ -4412,7 +4430,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "Slett $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4421,7 +4439,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ er lagt til", "placeholders": { "label": { "content": "$1", @@ -4543,7 +4561,7 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "Gjenstandens plassering" }, "fileSend": { "message": "File Send" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4579,16 +4591,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Privat nøkkel" }, "sshPublicKey": { - "message": "Public key" + "message": "Offentlig nøkkel" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Fingeravtrykk" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Nøkkeltype" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4609,22 +4621,22 @@ "message": "Minimum custom timeout is 1 minute." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "Ytterligere innhold er tilgjengelig" }, "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { - "message": "Show character count" + "message": "Vis tegntelleren" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Skjul tegntelleren" }, "itemsInTrash": { - "message": "Items in trash" + "message": "Gjenstander i papirkurven" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "Ingen gjenstander i papirkurven" }, "noItemsInTrashDesc": { "message": "Items you delete will appear here and be permanently deleted after 30 days" @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autentiserer" }, @@ -4653,11 +4692,11 @@ "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Vil du lagre påloggingen i Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Mellomrom", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { @@ -4665,15 +4704,15 @@ "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Baklengs apostrof", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Utropstegn", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Alfakrøll", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { @@ -4681,11 +4720,11 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Dollartegn", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Prosenttegn", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { @@ -4693,7 +4732,7 @@ "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Prosenttegn", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { @@ -4701,23 +4740,23 @@ "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Venstre parantes", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Høyre parantes", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Understrek", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Bindestrek", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Plusstegn", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { @@ -4725,19 +4764,19 @@ "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Venstre krøllparentes", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Høyre krøllparentes", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Venstre firkantparantes", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Høyre firkantparantes", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { @@ -4745,69 +4784,69 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Skråstrek bakover", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Kolon", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Semikolon", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Hermetegn", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Apostrofe", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Mindre enn", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Større enn", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Komma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Tidsperiode", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Spørsmålstegn", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Skråstrek", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Små bokstaver" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Store bokstaver" }, "generatedPassword": { - "message": "Generated password" + "message": "Generert passord" }, "compactMode": { - "message": "Compact mode" + "message": "Kompakt modus" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Viktig melding" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Sett opp 2-trinnspålogging" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -4828,7 +4867,7 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nei, det gjør jeg ikke" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { "message": "Yes, I can reliably access my email" @@ -4837,15 +4876,21 @@ "message": "Turn on two-step login" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Endre kontoens E-postadresse" }, "extensionWidth": { - "message": "Extension width" + "message": "Utvidelsens bredde" }, "wide": { - "message": "Wide" + "message": "Bred" }, "extraWide": { - "message": "Extra wide" + "message": "Ekstra bred" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 6ab3755c8f4..a228cf8ff55 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index d167ba220c1..d2e323aff4f 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk all je wachtwoorden, passkeys en gevoelige informatie", + "message": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk al je wachtwoorden, passkeys en gevoelige informatie", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -38,7 +38,7 @@ "message": "Rond het aanmaken van je account af met het instellen van een wachtwoord" }, "enterpriseSingleSignOn": { - "message": "Single sign-on voor bedrijven" + "message": "Enterprise Single Sign-On" }, "cancel": { "message": "Annuleren" @@ -409,7 +409,7 @@ "message": "Ontdek Bitwarden community-forums" }, "contactSupport": { - "message": "Contacteer Bitwarden support" + "message": "Contacteer Bitwarden ondersteuning" }, "sync": { "message": "Synchroniseren" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Wachtwoordzin genereren" }, + "passwordGenerated": { + "message": "Wachtwoord gegenereerd" + }, + "passphraseGenerated": { + "message": "Wachtwoorden gegenereerd" + }, + "usernameGenerated": { + "message": "Gebruikersnaam gegenereerd" + }, + "emailGenerated": { + "message": "E-mail gegenereerd" + }, "regeneratePassword": { "message": "Wachtwoord opnieuw genereren" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identiteit verifiëren" }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "yourVaultIsLocked": { "message": "Je kluis is vergrendeld. Bevestig je identiteit om door te gaan." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Vraag om een item toe te voegen als het niet is gevonden is je kluis. Dit geld voor alle ingelogde accounts." }, - "showCardsInVaultView": { - "message": "Kaarten als Autofill-suggesties in de kluisweergave weergeven" + "showCardsInVaultViewV2": { + "message": "Kaarten altijd als Autofill-suggesties in de kluisweergave weergeven" }, "showCardsCurrentTab": { "message": "Kaarten weergeven op tabpagina" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kaartenitems weergeven op de tabpagina voor gemakkelijk automatisch invullen." }, - "showIdentitiesInVaultView": { - "message": "Identiteiten als Autofill-suggesties in de kluisweergave weergeven" + "showIdentitiesInVaultViewV2": { + "message": "Identiteiten altijd als Autofill-suggesties in de kluisweergave weergeven" }, "showIdentitiesCurrentTab": { "message": "Identiteiten weergeven op tabpagina" @@ -2324,6 +2342,9 @@ "message": "Domeinen", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Geblokkeerde domeinen" + }, "excludedDomains": { "message": "Uitgesloten domeinen" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden zal voor deze domeinen niet vragen om de wachtwoorden op te slaan voor alle ingelogde accounts. Je moet de pagina verversen om de wijzigingen op te slaan." }, + "blockedDomainsDesc": { + "message": "Autofill en andere gerelateerde functies worden niet aangeboden voor deze websites. Vernieuw de pagina om de wijzigingen toe te passen." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is geblokkeerd voor deze website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Dit aanpassen in instellingen" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Wijzigingen in geblokkeerde domeinen opgeslagen" + }, "excludedDomainsSavedSuccess": { "message": "Uitgesloten domeinwijzigingen opgeslagen" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Fout" }, + "decryptionError": { + "message": "Ontsleutelingsfout" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kon de onderstaande kluisitem(s) niet ontsleutelen." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Neem contact op met de klantenservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "om extra dataverlies te voorkomen.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Gebruikersnamen genereren" }, @@ -3396,38 +3443,6 @@ "message": "In-/Uitklappen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Gegevens importeren naar Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Je LastPass-gegevens beschermen en naar Bitwarden importeren?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Als niet-versleuteld bestand opslaan", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Naar Bitwarden importeren", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importeren…", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Gegevens succesvol geïmporteerd!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fout bij importeren. Controleer console voor meer informatie.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netwerkfout opgetreden tijdens het importeren.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomein" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Suggesties automatisch invullen" }, + "itemSuggestions": { + "message": "Voorgestelde items" + }, "autofillSuggestionsTip": { "message": "Inlogitem opslaan voor automatisch invullen op deze site" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Tekst-Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden heeft een nieuw uiterlijk!" - }, - "bitwardenNewLookDesc": { - "message": "Automatisch invullen en zoeken is makkelijker en intuïtiever dan ooit vanaf het tabblad Kluis. Kijk rond!" - }, "accountActions": { "message": "Accountacties" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Je hebt geen toestemming om dit item te bewerken" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat pincode of wachtwoordontgrendeling eerst vereist is." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat de Bitwarden-desktopapp is afgesloten." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat het niet is ingeschakeld voor $EMAIL$ in de Bitwarden-desktopapp.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar om een onbekende reden." + }, "authenticating": { "message": "Aan het inloggen" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra breed" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Werk je desktopapplicatie bij" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Als u biometrische gegevens wilt gebruiken, moet je de desktopapplicatie bijwerken of vingerafdrukontgrendeling uitschakelen in de instellingen van de desktopapplicatie." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 6ab3755c8f4..a228cf8ff55 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 6ab3755c8f4..a228cf8ff55 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index a429059ea7d..60d454c6d88 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -20,7 +20,7 @@ "message": "Utwórz konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nowy użytkownik Bitwarden?" }, "logInWithPasskey": { "message": "Zaloguj się używając passkey" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Wygenruj frazę zabezpieczającą" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Wygeneruj ponownie hasło" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Zweryfikuj tożsamość" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Poproś o dodanie elementu, jeśli nie zostanie znaleziony w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." }, - "showCardsInVaultView": { - "message": "Pokaż karty jako sugestie autouzupełniania w widoku sejfu" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Pokaż karty na stronie głównej" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, - "showIdentitiesInVaultView": { - "message": "Pokaż tożsamości jako sugestie autouzupełniania w widoku sejfu" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" @@ -1320,7 +1338,7 @@ "message": "Limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Upłynął limit czasu uwierzytelniania. Uruchom ponownie proces logowania." }, "enterVerificationCodeEmail": { "message": "Wpisz 6-cyfrowy kod weryfikacyjny, który został przesłany na adres $EMAIL$.", @@ -2324,6 +2342,9 @@ "message": "Domeny", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Zablokowane domeny" + }, "excludedDomains": { "message": "Wykluczone domeny" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Aplikacja Bitwarden nie będzie proponować zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Musisz odświeżyć stronę, aby zastosowywać zmiany." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autouzupełnianie jest zablokowane dla tej witryny." + }, + "autofillBlockedNoticeGuidance": { + "message": "Zmień to w ustawieniach" + }, "websiteItemLabel": { "message": "Strona internetowa $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Zmiany w zablokowanych domenach zapisane" + }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" }, @@ -2553,7 +2586,7 @@ "message": "Aby wybrać plik za pomocą przeglądarki Safari, otwórz rozszerzenie w nowym oknie." }, "popOut": { - "message": "Pop out" + "message": "Odepnij" }, "sendFileCalloutHeader": { "message": "Zanim zaczniesz" @@ -2789,6 +2822,20 @@ "error": { "message": "Błąd" }, + "decryptionError": { + "message": "Błąd odszyfrowywania" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nie mógł odszyfrować elementów sejfu wymienionych poniżej." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Wygeneruj nazwę użytkownika" }, @@ -3080,10 +3127,10 @@ "message": "Powiadomienie zostało wysłane na twoje urządzenie" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Upewnij się, że Twoje konto jest odblokowane, a unikalny identyfikator konta pasuje do drugiego urządzenia" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Zostaniesz powiadomiony po zatwierdzeniu prośby" }, "needAnotherOptionV1": { "message": "Potrzebujesz innego sposobu?" @@ -3396,38 +3443,6 @@ "message": "Zwiń/rozwiń", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Zaimportować Twoje dane do Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Ochronić Twoje dane LastPass i zaimportować do Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Zapisz jako niezaszyfrowany plik", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importuj do Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importowanie...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dane pomyślnie zaimportowane!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Błąd podczas importowania. Sprawdź konsolę, aby uzyskać szczegółowe informacje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Wystąpił błąd sieci podczas importu.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domena aliasu" }, @@ -3979,7 +3994,10 @@ "message": "Passkey został usunięty" }, "autofillSuggestions": { - "message": "Sugestie autouzupełnienia" + "message": "Sugestie autouzupełniania" + }, + "itemSuggestions": { + "message": "Sugerowane elementy" }, "autofillSuggestionsTip": { "message": "Zapisz element logowania dla tej witryny, aby automatycznie wypełnić" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden ma nowy wygląd!" - }, - "bitwardenNewLookDesc": { - "message": "Auto wypełnianie i szukanie na zakładce sejfu jest teraz prostsze i bardziej intuicyjne. Rozejrzyj się tam!" - }, "accountActions": { "message": "Akcje konta" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Nie masz uprawnień do edycji tego elementu" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Odblokowanie biometryczne jest obecnie niedostępne." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ aplikacja desktopowa Bitwarden jest zamknięta." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Odblokowanie biometryczne jest niedostępne, ponieważ nie jest włączone dla $EMAIL$ w aplikacji desktopowej Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Odblokowanie biometryczne jest obecnie niedostępne z nieznanego powodu." + }, "authenticating": { "message": "Uwierzytelnianie" }, @@ -4804,22 +4843,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Ważna informacja" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Skonfiguruj dwustopniowe logowanie" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden wyśle kod na Twój adres e-mail w celu zweryfikowania logowania z nowych urządzeń, począwszy od lutego 2025 r." }, "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": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail do którego masz dostęp." }, "remindMeLater": { - "message": "Remind me later" + "message": "Przypomnij mi później" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Czy masz pewny dostęp do swojego adresu e-mail, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4828,16 +4867,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nie, nie mam" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Tak, mam pewny dostęp do mojego adresu e-mail" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Włącz dwustopniowe logowanie" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Zmień adres e-mail konta" }, "extensionWidth": { "message": "Szerokość rozszerzenia" @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Bardzo szerokie" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index cd0c9979103..050df835041 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Gerar frase secreta" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Gerar Nova Senha" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar Identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seu cofre está trancado. Verifique sua identidade para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se um não for encontrado no seu cofre. Aplica-se a todas as contas logadas." }, - "showCardsInVaultView": { - "message": "Mostrar cartões como sugestões de preenchimento automático na exibição do Cofre" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar cartões em páginas com guias." @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Exibir itens de cartão em páginas com abas para simplificar o preenchimento automático" }, - "showIdentitiesInVaultView": { - "message": "Mostrar identifica como sugestões de preenchimento automático na exibição do Cofre" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Exibir Identidades na Aba Atual" @@ -2324,6 +2342,9 @@ "message": "Domínios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domínios Excluídos" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não irá pedir para salvar os detalhes de credencial para estes domínios. Você deve atualizar a página para que as alterações entrem em vigor." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Site $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Mudanças de domínios excluídos salvas" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Gerar Usuário" }, @@ -3396,38 +3443,6 @@ "message": "Alternar colapso", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar seus dados para o Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteja seus dados do LastPass e importe para o Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salvar como arquivo não criptografado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar para o Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dados importados com sucesso!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro ao importar. Verifique o console para detalhes.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Erro de rede encontrado durante a importação.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias do domínio" }, @@ -3979,7 +3994,10 @@ "message": "Chave de acesso removida" }, "autofillSuggestions": { - "message": "Sugestões de autopreenchimento" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Salvar um item de login para este site autopreenchimento" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Texto enviado" }, - "bitwardenNewLook": { - "message": "Bitwarden tem uma nova aparência!" - }, - "bitwardenNewLookDesc": { - "message": "É mais fácil e mais intuitivo do que nunca autopreenchimento e pesquise na guia Cofre. Dê uma olhada ao redor!" - }, "accountActions": { "message": "Ações da conta" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Você não tem permissão para editar este arquivo" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autenticando" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra Grande" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index def50289ae6..b12204dd3c9 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Gerar frase de acesso" }, + "passwordGenerated": { + "message": "Palavra-passe gerada" + }, + "passphraseGenerated": { + "message": "Frase de acesso gerada" + }, + "usernameGenerated": { + "message": "Nome de utilizador gerado" + }, + "emailGenerated": { + "message": "E-mail gerado" + }, "regeneratePassword": { "message": "Regenerar palavra-passe" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "yourVaultIsLocked": { "message": "O seu cofre está bloqueado. Verifique a sua identidade para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se não for encontrado um no seu cofre. Aplica-se a todas as contas com sessão iniciada." }, - "showCardsInVaultView": { - "message": "Mostrar cartões como sugestões de preenchimento automático na vista do cofre" + "showCardsInVaultViewV2": { + "message": "Mostrar sempre cartões como sugestões de preenchimento automático na vista do cofre" }, "showCardsCurrentTab": { "message": "Mostrar cartões na página Separador" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listar itens de cartões na página Separador para facilitar o preenchimento automático." }, - "showIdentitiesInVaultView": { - "message": "Mostrar identidades como sugestões de preenchimento automático na vista do cofre" + "showIdentitiesInVaultViewV2": { + "message": "Mostrar sempre identidades como sugestões de preenchimento automático na vista do cofre" }, "showIdentitiesCurrentTab": { "message": "Mostrar identidades na página Separador" @@ -2324,6 +2342,9 @@ "message": "Domínios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domínios bloqueados" + }, "excludedDomains": { "message": "Domínios excluídos" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não pedirá para guardar os detalhes de início de sessão destes domínios para todas as contas com sessão iniciada. É necessário atualizar a página para que as alterações tenham efeito." }, + "blockedDomainsDesc": { + "message": "O preenchimento automático e outras funcionalidades relacionadas não serão disponibilizados para estes sites. É necessário atualizar a página para que as alterações tenham efeito." + }, + "autofillBlockedNoticeV2": { + "message": "O preenchimento automático está bloqueado para este site." + }, + "autofillBlockedNoticeGuidance": { + "message": "Alterar esta opção nas definições" + }, "websiteItemLabel": { "message": "Site $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Alterações do domínio bloqueado guardadas" + }, "excludedDomainsSavedSuccess": { "message": "Alterações do domínio excluído guardadas" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Erro de desencriptação" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "O Bitwarden não conseguiu desencriptar o(s) item(ns) do cofre listado(s) abaixo." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacte o serviço de apoio ao cliente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "para evitar perdas adicionais de dados.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Gerar nome de utilizador" }, @@ -3396,38 +3443,6 @@ "message": "Alternar colapso", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar os seus dados para o Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteger os seus dados LastPass e importar para o Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guardar como ficheiro não encriptado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar para o Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "A importar...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dados importados com sucesso!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro de importação. Verifique a consola para obter detalhes.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Erro de rede encontrado durante a importação.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias de domínio" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Sugestões de preenchimento automático" }, + "itemSuggestions": { + "message": "Itens sugeridos" + }, "autofillSuggestionsTip": { "message": "Guarde uma credencial deste site para preenchimento automático" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Sends de texto" }, - "bitwardenNewLook": { - "message": "O Bitwarden tem um novo visual!" - }, - "bitwardenNewLookDesc": { - "message": "É mais fácil e mais intuitivo do que nunca preencher automaticamente e pesquisar a partir do separador Cofre. Dê uma vista de olhos!" - }, "accountActions": { "message": "Ações da conta" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Não tem permissão para editar este item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "O desbloqueio biométrico não está disponível porque o desbloqueio por PIN ou palavra-passe é necessário primeiro." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "O desbloqueio biométrico está atualmente indisponível." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "O desbloqueio biométrico não está disponível porque a app para computador Bitwarden está fechada." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "O desbloqueio biométrico não está disponível porque não está ativado para $EMAIL$ na app Bitwarden para computador.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "O desbloqueio biométrico está atualmente indisponível por um motivo desconhecido." + }, "authenticating": { "message": "A autenticar" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Muito ampla" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Por favor, atualize a sua aplicação para computador" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Para utilizar o desbloqueio biométrico, atualize a sua aplicação para computador ou desative o desbloqueio por impressão digital nas definições dessa mesma app." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 58c0a313f69..24826693796 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerare parolă" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificare identitate" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seiful dvs. este blocat. Verificați-vă identitatea pentru a continua." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afișați cardurile pe pagina Filă" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listați elementele cardului pe pagina Filă pentru a facilita completarea automată." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afișați identitățile pe pagina Filă" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domenii excluse" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Eroare" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generare nume de utilizator" }, @@ -3396,38 +3443,6 @@ "message": "Comutare restrângere", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index acdee563ceb..b1946be68b9 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Создать парольную фразу" }, + "passwordGenerated": { + "message": "Пароль создан" + }, + "passphraseGenerated": { + "message": "Парольная фраза создана" + }, + "usernameGenerated": { + "message": "Имя пользователя создано" + }, + "emailGenerated": { + "message": "Email создан" + }, "regeneratePassword": { "message": "Создать новый пароль" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Подтвердить личность" }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "yourVaultIsLocked": { "message": "Ваше хранилище заблокировано. Подтвердите свою личность, чтобы продолжить" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Запрос на добавление элемента, если он отсутствует в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, - "showCardsInVaultView": { - "message": "Показывать карты как предложение автозаполнения при просмотре Хранилище" + "showCardsInVaultViewV2": { + "message": "Всегда показывать карты как предложения автозаполнения при просмотре хранилища" }, "showCardsCurrentTab": { "message": "Показывать карты на вкладке" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Карты будут отображены на вкладке для удобного автозаполнения." }, - "showIdentitiesInVaultView": { - "message": "Показывать личности как предложение автозаполнения при просмотре Хранилище" + "showIdentitiesInVaultViewV2": { + "message": "Всегда показывать личности как предложения автозаполнения при просмотре хранилища" }, "showIdentitiesCurrentTab": { "message": "Показывать Личности на вкладке" @@ -2324,6 +2342,9 @@ "message": "Домены", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Заблокированные домены" + }, "excludedDomains": { "message": "Исключенные домены" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не будет предлагать сохранение логинов для этих доменов для всех авторизованных аккаунтов. Для вступления изменений в силу необходимо обновить страницу." }, + "blockedDomainsDesc": { + "message": "Автозаполнение и другие связанные с ним функции не будут предлагаться для этих сайтов. Чтобы изменения вступили в силу, необходимо обновить страницу." + }, + "autofillBlockedNoticeV2": { + "message": "Автозаполнение для этого сайта заблокировано." + }, + "autofillBlockedNoticeGuidance": { + "message": "Измените это в настройках" + }, "websiteItemLabel": { "message": "Сайт $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Изменения в заблокированном домене сохранены" + }, "excludedDomainsSavedSuccess": { "message": "Изменения в исключенном домене сохранены" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Ошибка" }, + "decryptionError": { + "message": "Ошибка расшифровки" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не удалось расшифровать элемент(ы) хранилища, перечисленные ниже." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Обратитесь в службу поддержки,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "чтобы избежать дополнительной потери данных.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Создать имя пользователя" }, @@ -3396,38 +3443,6 @@ "message": "Свернуть/развернуть", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Импортировать данные в Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Защитить данные LastPass и импортировать их в Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Сохранить как незашифрованный файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Импортировать в Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Импорт...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Данные успешно импортированы!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Ошибка импорта. Проверьте консоль для получения подробной информации.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Во время импорта возникла сетевая ошибка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдоним домена" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Предложения по автозаполнению" }, + "itemSuggestions": { + "message": "Предлагаемые элементы" + }, "autofillSuggestionsTip": { "message": "Сохранить логин для этого сайта для автозаполнения" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Текстовая Send" }, - "bitwardenNewLook": { - "message": "У Bitwarden новый облик!" - }, - "bitwardenNewLookDesc": { - "message": "Теперь автозаполнение и поиск на вкладке Хранилище стали проще и интуитивно понятнее, чем когда-либо. Осмотритесь!" - }, "accountActions": { "message": "Действия аккаунта" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "У вас нет разрешения на редактирование этого элемента" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Биометрическая разблокировка недоступна, поскольку сначала требуется разблокировка с помощью PIN-кода или пароля." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Биометрическая разблокировка в настоящее время недоступна." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Биометрическая разблокировка недоступна, поскольку Bitwarden для компьютера закрыт." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Биометрическая разблокировка недоступна, потому что она не включена для $EMAIL$ в приложении Bitwarden для компьютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Биометрическая разблокировка в настоящее время недоступна по неизвестной причине." + }, "authenticating": { "message": "Аутентификация" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Очень широкое" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Пожалуйста, обновите приложение для компьютера" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Чтобы использовать биометрическую разблокировку, обновите приложение для компьютера или отключите разблокировку по отпечатку пальца в настройках компьютера." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index b25c2fd30d5..1d0a40e167c 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "මුරපදය ප්රතිජනනය" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "අනන්යතාවය සත්යාපනය කරන්න" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ඔබේ සුරක්ෂිතාගාරය අගුළු දමා ඇත. දිගටම කරගෙන යාමට ඔබේ අනන්යතාවය සත්යාපනය කරන්න." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "බැහැර වසම්" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 5d11227f003..ab6de031efc 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generovať prístupovú frázu" }, + "passwordGenerated": { + "message": "Heslo vygenerované" + }, + "passphraseGenerated": { + "message": "Prístupová fráza vygenerovaná" + }, + "usernameGenerated": { + "message": "Používateľské meno vygenerované" + }, + "emailGenerated": { + "message": "E-mail vygenoravný" + }, "regeneratePassword": { "message": "Vygenerovať nové heslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Overiť identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zariadenie nepoznáme. Na overenie vašej totožnosti zadajte kód, ktorý bol zaslaný na váš e-mail." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamknutý. Ak chcete pokračovať, overte svoju identitu." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Požiada o pridanie položky, ak sa v trezore nenachádza. Platí pre všetky prihlásené účty." }, - "showCardsInVaultView": { - "message": "Zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" + "showCardsInVaultViewV2": { + "message": "Vždy zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" }, "showCardsCurrentTab": { "message": "Zobraziť karty na stránke \"Aktuálna karta\"" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Zoznam položiek karty na stránke \"Aktuálna karta\" na jednoduché automatické vyplnenie." }, - "showIdentitiesInVaultView": { - "message": "Zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" }, "showIdentitiesCurrentTab": { "message": "Zobraziť identity na stránke \"Aktuálna karta\"" @@ -2324,6 +2342,9 @@ "message": "Domény", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokované domény" + }, "excludedDomains": { "message": "Vylúčené domény" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude požadovať ukladanie prihlasovacích údajov pre tieto domény pre všetky prihlásené účty. Aby sa zmeny prejavili, musíte stránku obnoviť." }, + "blockedDomainsDesc": { + "message": "Automatické vypĺňanie a ďalšie súvisiace funkcie sa na týchto webových stránkach nebudú ponúkať. Aby sa zmeny prejavili, musíte stránku obnoviť." + }, + "autofillBlockedNoticeV2": { + "message": "Automatické vypĺňanie je pre túto webovú stránku zablokované." + }, + "autofillBlockedNoticeGuidance": { + "message": "Zmeňte to v nastaveniach" + }, "websiteItemLabel": { "message": "Webstránka $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Zmeny v blokovaných doménach boli uložené" + }, "excludedDomainsSavedSuccess": { "message": "Uložené zmeny vylúčenej domény" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Chyba" }, + "decryptionError": { + "message": "Chyba dešifrovania" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nedokázal dešifrovať nižšie uvedené položky trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznícku podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "aby ste predišli ďalším stratám údajov.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Vygenerovať používateľské meno" }, @@ -3396,38 +3443,6 @@ "message": "Prepnúť zbalenie", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importovať údaje do Bitwardenu?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Ochrániť údaje z LastPassu a importovať ich do Bitwardenu?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Uložiť ako nezašifrovaný súbor", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importovať do Bitwardenu", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importovanie...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Import údajov prebehol úspešne!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Chyba pri importovaní. Podrobnosti nájdete v konzole.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Počas importu sa vyskytla chyba siete.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias doména" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Návrhy automatického vypĺňania" }, + "itemSuggestions": { + "message": "Navrhované položky" + }, "autofillSuggestionsTip": { "message": "Uložte položku prihlásenia pre tento web na automatické vyplnenie" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Textové Sendy" }, - "bitwardenNewLook": { - "message": "Bitwarden má nový vzhľad!" - }, - "bitwardenNewLookDesc": { - "message": "Automatické vypĺňanie a vyhľadávanie na karte Trezor je jednoduchšie a intuitívnejšie ako kedykoľvek predtým. Poobzerajte sa!" - }, "accountActions": { "message": "Operácie s účtom" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Na úpravu tejto položky nemáte oprávnenie" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné pretože je najskôr potrebné odomykanie pomocou PIN alebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Odomykanie biometrickými údajmi je momentálne nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Odomykanie biometrickými údajmi je nedostupné, pretože aplikácia Bitwarden pre desktop je zatvorená." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Odomykanie biometrickými údajmi je nedostupné, pretože nie je povolené pre $EMAIL$ v aplikácii Bitwarden pre desktop.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Odomykanie biometrickými údajmi je momentálne z neznámych dôvodov nedostupné." + }, "authenticating": { "message": "Overuje sa" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra široké" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte desktopovú aplikáciu" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Ak chcete používať biometrické odomykanie, aktualizujte desktopovú aplikáciu alebo vypnite odomykanie odtlačkom prsta v nastaveniach desktopovej aplikácie." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 490f991d252..af594791920 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ponovno ustvari geslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Preverjanje istovetnosti" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Če predmeta ni v trezorju, ga je potrebno dodati. Velja za vse prijavljene račune." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži kartice na strani Zavihek" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Na strani Zavihek prikaži kartice za lažje samodejno izpoljnjevanje." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete na strani Zavihek" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Izključene domene" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Napaka" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Ustvari uporabniško ime" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index d515c2a0c6b..811800511c0 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генеришите приступну фразу" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Поново генериши лозинку" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Потврдите идентитет" }, + "weDontRecognizeThisDevice": { + "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." + }, + "continueLoggingIn": { + "message": "Настави са пријављивањем" + }, "yourVaultIsLocked": { "message": "Сеф је закључан. Унесите главну лозинку за наставак." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Затражите да додате ставку ако она није пронађена у вашем сефу. Односи се на све пријављене налоге." }, - "showCardsInVaultView": { - "message": "Прикажите картице као предлоге за ауто-попуњавање у приказу сефа" + "showCardsInVaultViewV2": { + "message": "Увек приказуј картице као препоруке аутоматског попуњавања на приказу трезора" }, "showCardsCurrentTab": { "message": "Прикажи кредитне картице на страници картице" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Прикажи ставке кредитних картица на страници картице за лакше аутоматско допуњавање." }, - "showIdentitiesInVaultView": { - "message": "Прикажите идентитете као предлоге за ауто-попуњавање у приказу сефа" + "showIdentitiesInVaultViewV2": { + "message": "Увек приказуј идентитете као препоруке аутоматског попуњавања на приказу трезора" }, "showIdentitiesCurrentTab": { "message": "Прикажи идентитете на страници" @@ -2324,6 +2342,9 @@ "message": "Домени", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Блокирани домени" + }, "excludedDomains": { "message": "Изузети домени" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, + "blockedDomainsDesc": { + "message": "Аутоматско попуњавање и сродне функције неће бити понуђене за ове веб сајтове. Морате освежити страницу да би се измене примениле." + }, + "autofillBlockedNoticeV2": { + "message": "Аутоматско попуњавање је блокирано за овај веб сајт." + }, + "autofillBlockedNoticeGuidance": { + "message": "Промените ово у подешавањима" + }, "websiteItemLabel": { "message": "Сајт $number$ (УРЛ)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Измене блокираних домена су сачуване" + }, "excludedDomainsSavedSuccess": { "message": "Изузете промене домена су сачуване" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Грешка" }, + "decryptionError": { + "message": "Грешка при декрипцији" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генериши име" }, @@ -3396,38 +3443,6 @@ "message": "Промени проширење", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Увезите своје податке у Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Заштитите своје LastPass податке и увезите у Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Сачувати као нешифровану датотеку", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Увоз у Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Увоз...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Подаци су успешно увезени!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Грешка при увозу. Проверите конзолу за детаље.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Дошло је до грешке на мрежи током увоза.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Домен алијаса" }, @@ -3979,7 +3994,10 @@ "message": "Приступни кључ је уклоњен" }, "autofillSuggestions": { - "message": "Предлози за ауто-попуњавање" + "message": "Предлози аутоматског попуњавања" + }, + "itemSuggestions": { + "message": "Предложене ставке" }, "autofillSuggestionsTip": { "message": "Сачувајте ставку за пријаву за ову локацију за ауто-попуњавање" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Текст „Send“" }, - "bitwardenNewLook": { - "message": "Bitwarden има нови изглед!" - }, - "bitwardenNewLookDesc": { - "message": "Лакше је и интуитивније него икада да се аутоматски попуњава и тражи са картице Сефа. Проверите!" - }, "accountActions": { "message": "Акције везане за налог" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Немате дозволу да уређујете ову ставку" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Биометријско откључавање није доступно јер је пре тога потребно унети ПИН или лозинку за откључавање." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Биометријско откључавање тренутно није доступно." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Биометријско откључавање није доступно јер је Bitwarden апликација на рачунару угашена." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Биометријско откључавање није доступно јер није омогућено за $EMAIL$ у Bitwarden апликацији на рачунару.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Биометријско откључавање није доступно из непознатог разлога." + }, "authenticating": { "message": "Аутентификација" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Врло широко" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Молим вас надоградите вашу апликацију на рачунару" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Да би сте користили биометријско откључавање, надоградите вашу апликацију на рачунару, или онемогућите откључавање отиском прста у подешавањима на рачунару." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 84de9bbfa05..fb377af7679 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generera lösenfras" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Återskapa lösenord" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifiera identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ditt valv är låst. Verifiera din identitet för att fortsätta." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Visa kort på fliksida" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Lista kortobjekt på fliksidan för enkel automatisk fyllning." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Visa identiteter på fliksidan" @@ -2324,6 +2342,9 @@ "message": "Domäner", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Exkluderade domäner" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Webbplats $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Fel" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generera användarnamn" }, @@ -3396,38 +3443,6 @@ "message": "Växla synlig/dold", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importera din data till Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Skydda din LastPass-data och importera till Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Spara som okrypterad fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importera till Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerar...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data har importerats till ditt valv!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fel vid importeringen. Kolla konsolen för detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Nätverksfel uppstod vid import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomän" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden har fått ett nytt utseende!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Kontoåtgärder" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 6ab3755c8f4..a228cf8ff55 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index d9ff1c3f076..d97d992674c 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "ยืนยันตัวตน" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ตู้เซฟของคุณถูกล็อก ยืนยันตัวตนของคุณเพื่อดำเนินการต่อ" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "แสดงการ์ดบนหน้าแท็บ" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "บัตรรายการในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "แสดงตัวตนบนหน้าแท็บ" @@ -2324,6 +2342,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3396,38 +3443,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index e1236b3f86d..bc16c627b5b 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -385,7 +385,7 @@ "message": "Hiç klasör eklenmedi" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "Kasanızdaki kayıtları organize etmek için klasörler oluşturun" }, "deleteFolderPermanently": { "message": "Bu klasörü kalıcı olarak silmek istediğinizden emin misiniz?" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Parola üret" }, + "passwordGenerated": { + "message": "Parola üretildi" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Kullanıcı adı üretildi" + }, + "emailGenerated": { + "message": "E-posta üretildi" + }, "regeneratePassword": { "message": "Yeni parola oluştur" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Kimliği doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "yourVaultIsLocked": { "message": "Kasanız kilitli. Devam etmek için kimliğinizi doğrulayın." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Kasanızda bulunmayan kayıtların eklenmesini isteyip istemediğinizi sorar. Oturum açmış tüm hesaplar için geçerlidir." }, - "showCardsInVaultView": { - "message": "Kasa görünümünde kartları otomatik doldurma önerisi olarak göster" + "showCardsInVaultViewV2": { + "message": "Kasa görünümünde kartları her zaman otomatik doldurma önerisi olarak göster" }, "showCardsCurrentTab": { "message": "Sekme sayfasında kartları göster" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kolay otomatik doldurma için sekme sayfasında kartları listele." }, - "showIdentitiesInVaultView": { - "message": "Kasa görünümünde kimlikleri otomatik doldurma önerisi olarak göster" + "showIdentitiesInVaultViewV2": { + "message": "Kasa görünümünde kimlikleri her zaman otomatik doldurma önerisi olarak göster" }, "showIdentitiesCurrentTab": { "message": "Sekme sayfasında kimlikleri göster" @@ -1275,7 +1293,7 @@ "message": "Bitwarden'ı desteklediğiniz için teşekkür ederiz." }, "premiumFeatures": { - "message": "Premium'a yükseltin ve şunları alın:" + "message": "Premium'a geçmenin avantajları:" }, "premiumPrice": { "message": "Bunların hepsi sadece yılda $PRICE$!", @@ -1466,7 +1484,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" }, "showInlineMenuLabel": { "message": "Form alanlarında otomatik doldurma önerilerini göster" @@ -1502,13 +1520,13 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "enableAutoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "enableAutoFillOnPageLoadDesc": { - "message": "Sayfa yüklendiğinde giriş formu tespit edilirse otomatik olarak formu doldur." + "message": "Sayfa yüklenince giriş formu tespit edilirse otomatik olarak formu doldur." }, "experimentalFeature": { "message": "Ele geçirilmiş veya güvenilmeyen web siteleri sayfa yüklenirken otomatik doldurmayı suistimal edebilir." @@ -1523,19 +1541,19 @@ "message": "Hesaplar için varsayılan otomatik doldurma ayarı" }, "defaultAutoFillOnPageLoadDesc": { - "message": "\"Sayfa yüklendiğinde otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." + "message": "\"Sayfa yüklenince otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." }, "itemAutoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur (Seçeneklerde ayarlanmışsa)" + "message": "Sayfa yüklenince otomatik doldur (Seçeneklerde ayarlanmışsa)" }, "autoFillOnPageLoadUseDefault": { "message": "Varsayılan ayarı kullan" }, "autoFillOnPageLoadYes": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "autoFillOnPageLoadNo": { - "message": "Sayfa yüklendiğinde otomatik doldurma" + "message": "Sayfa yüklenince otomatik doldurma" }, "commandOpenPopup": { "message": "Kasayı açılır pencerede aç" @@ -2324,6 +2342,9 @@ "message": "Alan adları", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Engellenen alan adları" + }, "excludedDomains": { "message": "Hariç tutulan alan adları" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, oturum açmış tüm hesaplar için bu alan adlarının hesap bilgilerini kaydetmeyi sormayacaktır. Değişikliklerin etkili olması için sayfayı yenilemeniz gerekir." }, + "blockedDomainsDesc": { + "message": "Bu siteler için otomatik doldurma ve diğer ilgili özellikler önerilmeyecektir. Değişikliklerin devreye girmesi için sayfayı yenilemelisiniz." + }, + "autofillBlockedNoticeV2": { + "message": "Bu sitede otomatik doldurma engellenmiş." + }, + "autofillBlockedNoticeGuidance": { + "message": "Bunu ayarlardan değiştirebilirsiniz" + }, "websiteItemLabel": { "message": "Web sitesi $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Engelli alan adı değişiklikleri kaydedildi" + }, "excludedDomainsSavedSuccess": { "message": "Alan adı istisnası değişiklikleri kaydedildi" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Hata" }, + "decryptionError": { + "message": "Şifre çözme sorunu" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Kullanıcı adı oluştur" }, @@ -3122,7 +3169,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Kuruluş ilkeleriniz, sayfa yüklendiğinde otomatik doldurmayı etkinleştirdi." + "message": "Kuruluş ilkeleriniz, sayfa yüklenince otomatik doldurmayı etkinleştirdi." }, "howToAutofill": { "message": "Otomatik doldurma nasıl yapılır?" @@ -3396,47 +3443,15 @@ "message": "Daraltmayı aç/kapat", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Verileriniz Bitwarden'a aktarılsın mı?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass verileriniz korunsun ve Bitwarden'a aktarılsın mı?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Şifrelenmemiş dosya olarak kaydet", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden'a aktar", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "İçe aktarılıyor...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Veriler başarıyla içe aktarıldı!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "İçe aktarma hatası. Ayrıntılar için konsolu kontrol edin.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "İçe aktarma sırasında ağ hatasıyla karşılaşıldı.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias alan adı" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklendiğinde otomatik olarak doldurulamaz. Sayfa yüklendiğinde otomatik doldurma kapatıldı.", + "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklenince otomatik olarak doldurulamaz. Sayfa yüklenince otomatik doldurma kapatıldı.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Sayfa yüklendiğinde otomatik doldurma, varsayılan ayarı kullanacak şekilde ayarlandı.", + "message": "Sayfa yüklenince otomatik doldurma, varsayılan ayarı kullanacak şekilde ayarlandı.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { @@ -3711,7 +3726,7 @@ "message": "Geçiş anahtarı" }, "accessing": { - "message": "Erişim" + "message": "Erişilen konum:" }, "loggedInExclamation": { "message": "Giriş yapıldı!" @@ -3979,7 +3994,10 @@ "message": "Geçiş anahtarı kaldırıldı" }, "autofillSuggestions": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" + }, + "itemSuggestions": { + "message": "Önerilen kayıtlar" }, "autofillSuggestionsTip": { "message": "Otomatik doldurma için bu siteye ait bir hesap kaydededin" @@ -4304,7 +4322,7 @@ } }, "autoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "cardExpiredTitle": { "message": "Kartın süresi dolmuş" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Metin Send'leri" }, - "bitwardenNewLook": { - "message": "Bitwarden'ın tasarımı güncellendi!" - }, - "bitwardenNewLookDesc": { - "message": "Otomatik doldurma ve kasanızda arama yapma artık eskisinden daha kolay. Yeni tasarıma göz atmayı unutmayın!" - }, "accountActions": { "message": "Hesap işlemleri" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Bu kaydı düzenleme yetkisine sahip değilsiniz" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Kimlik doğrulanıyor" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra geniş" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfen masaüstü uygulamanızı güncelleyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 8e20bc56ff5..f14c252e52d 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генерувати парольну фразу" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Генерувати новий" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Виконати перевірку" }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "yourVaultIsLocked": { "message": "Ваше сховище заблоковане. Для продовження виконайте перевірку." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Запитувати про додавання запису, якщо такого не знайдено у вашому сховищі. Застосовується для всіх облікових записів, до яких виконано вхід." }, - "showCardsInVaultView": { - "message": "Показувати картки як пропозиції автозаповнення в режимі перегляду сховища" + "showCardsInVaultViewV2": { + "message": "Завжди показувати картки як пропозиції автозаповнення в режимі перегляду сховища" }, "showCardsCurrentTab": { "message": "Показувати картки на вкладці" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Показувати список карток на сторінці вкладки для легкого автозаповнення." }, - "showIdentitiesInVaultView": { - "message": "Показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" + "showIdentitiesInVaultViewV2": { + "message": "Завжди показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" }, "showIdentitiesCurrentTab": { "message": "Показувати посвідчення на вкладці" @@ -2324,6 +2342,9 @@ "message": "Домени", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Заблоковані домени" + }, "excludedDomains": { "message": "Виключені домени" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не запитуватиме про збереження даних входу для цих доменів для всіх облікових записів, до яких виконано вхід. Потрібно оновити сторінку для застосування змін." }, + "blockedDomainsDesc": { + "message": "Автозаповнення та інші пов'язані функції не пропонуватимуться для цих вебсайтів. Вам слід оновити сторінку для застосування змін." + }, + "autofillBlockedNoticeV2": { + "message": "Автозаповнення для цього вебсайту заблоковано." + }, + "autofillBlockedNoticeGuidance": { + "message": "Змінити в налаштуваннях" + }, "websiteItemLabel": { "message": "Вебсайт $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Зміни заблокованих доменів збережено" + }, "excludedDomainsSavedSuccess": { "message": "Виняток для домену збережено" }, @@ -2376,7 +2409,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Надіслати подробиці", + "message": "Подробиці відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2789,6 +2822,20 @@ "error": { "message": "Помилка" }, + "decryptionError": { + "message": "Помилка розшифрування" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Зверніться до служби підтримки клієнтів,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "щоб уникнути втрати даних.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генерувати ім'я користувача" }, @@ -3396,38 +3443,6 @@ "message": "Згорнути/розгорнути", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Імпортувати ваші дані до Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Захистити ваші дані LastPass та імпортувати до Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Зберегти незашифрований файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Імпортувати до Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Триває імпортування...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Дані успішно імпортовано!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Помилка імпортування. Перевірте консоль для перегляду подробиць.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Під час імпортування сталася мережева помилка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдонім домену" }, @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "Пропозиції автозаповнення" }, + "itemSuggestions": { + "message": "Запропоновані записи" + }, "autofillSuggestionsTip": { "message": "Зберегти дані входу цього сайту для автозаповнення" }, @@ -4557,12 +4575,6 @@ "textSends": { "message": "Відправлення тексту" }, - "bitwardenNewLook": { - "message": "Bitwarden має новий вигляд!" - }, - "bitwardenNewLookDesc": { - "message": "Ще простіше автозаповнення та інтуїтивніший пошук у сховищі. Ознайомтеся!" - }, "accountActions": { "message": "Дії з обліковим записом" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "Вам не дозволено редагувати цей запис" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Біометричне розблокування недоступне, оскільки спочатку потрібно ввести PIN-код або пароль." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Біометричне розблокування наразі недоступне." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Біометричне розблокування недоступне, оскільки програму Bitwarden для комп'ютера закрито." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Біометричне розблокування недоступне, оскільки воно не увімкнене для $EMAIL$ у програмі Bitwarden для комп'ютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Біометричне розблокування зараз недоступне з невідомої причини." + }, "authenticating": { "message": "Аутентифікація" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Дуже широке" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Оновіть свою комп'ютерну програму" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Щоб використовувати біометричне розблокування, оновіть комп'ютерну програму, або вимкніть розблокування відбитком пальця в налаштуваннях системи." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 268b12a6254..78173858e82 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Tạo lại mật khẩu" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Xác minh danh tính" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Kho của bạn đã bị khóa. Xác minh danh tính của bạn để mở khoá." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, - "showCardsInVaultView": { - "message": "Hiển thị các thẻ như các gợi ý tự động điền trên giao diện kho" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Liệt kê các mục thẻ trên trang Tab để dễ dàng tự động điền." }, - "showIdentitiesInVaultView": { - "message": "Hiển thị các danh tính như các gợi ý tự động điền trên giao diện kho" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Hiển thị danh tính trên trang Tab" @@ -2324,6 +2342,9 @@ "message": "Các tên miền", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Tên miền đã loại trừ" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các miền này. Bạn phải làm mới trang để các thay đổi có hiệu lực." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Trang Web $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Các thay đổi tên miền loại trừ đã được lưu" }, @@ -2789,6 +2822,20 @@ "error": { "message": "Lỗi" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Tạo tên người dùng" }, @@ -3396,38 +3443,6 @@ "message": "Bật/tắt thu gọn", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Nhập dữ liệu của bạn vào Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Bảo vệ dữ liệu LastPass của bạn và nhập vào Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Lưu dưới dạng tập tin không được mã hóa", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Nhập vào Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Đang nhập...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dữ liệu đã được nhập thành công!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Xảy ra lỗi trong quá trình nhập. Kiểm tra bảng điều khiển để biết thêm chi tiết.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Đã xảy ra lỗi mạng trong quá trình nhập dữ liệu.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Tên miền thay thế" }, @@ -3979,7 +3994,10 @@ "message": "Đã xóa mã khoá" }, "autofillSuggestions": { - "message": "Gợi ý điền tự động" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Lưu thông tin đăng nhập cho trang này để tự động điền" @@ -4557,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 69c1194af47..ac43df7a5a4 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", + "message": "无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -290,7 +290,7 @@ "message": "前往帮助中心吗?" }, "continueToHelpCenterDesc": { - "message": "访问帮助中心了解更多如何使用 Bitwarden 的信息。" + "message": "在帮助中心进一步了解如何使用 Bitwarden。" }, "continueToBrowserExtensionStore": { "message": "前往浏览器扩展商店吗?" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "生成密码短语" }, + "passwordGenerated": { + "message": "密码已生成" + }, + "passphraseGenerated": { + "message": "密码短语已生成" + }, + "usernameGenerated": { + "message": "用户名已生成" + }, + "emailGenerated": { + "message": "电子邮箱已生成" + }, "regeneratePassword": { "message": "重新生成密码" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "验证身份" }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "yourVaultIsLocked": { "message": "您的密码库已锁定。请先验证您的身份。" }, @@ -834,7 +852,7 @@ "message": "Bitwarden 可以存储并填充两步验证码。选择相机图标来截取此网站的验证器二维码,或者手动复制并粘贴密钥到此字段。" }, "learnMoreAboutAuthenticators": { - "message": "了解更多关于验证器的信息" + "message": "进一步了解验证器" }, "copyTOTP": { "message": "复制验证器密钥 (TOTP)" @@ -855,13 +873,13 @@ "message": "登录到 Bitwarden" }, "restartRegistration": { - "message": "重新开始注册" + "message": "重启注册" }, "expiredLink": { "message": "失效链接" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "请重新注册或尝试登录。" + "message": "请重启注册或尝试登录。" }, "youMayAlreadyHaveAnAccount": { "message": "您可能已经有一个账户了" @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "如果在密码库中找不到项目,询问添加一个。适用于所有已登录的账户。" }, - "showCardsInVaultView": { - "message": "在密码库视图中将支付卡显示为自动填充建议" + "showCardsInVaultViewV2": { + "message": "在密码库视图中将支付卡始终显示为自动填充建议" }, "showCardsCurrentTab": { "message": "在标签页上显示支付卡" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "在标签页上列出支付卡项目,以便于自动填充。" }, - "showIdentitiesInVaultView": { - "message": "在密码库视图中将身份显示为自动填充建议" + "showIdentitiesInVaultViewV2": { + "message": "在密码库视图中将身份始终显示为自动填充建议" }, "showIdentitiesCurrentTab": { "message": "在标签页上显示身份" @@ -1514,10 +1532,10 @@ "message": "不完整或不信任的网站可以利用页面加载时的自动填充功能。" }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "了解更多关于风险的信息" + "message": "进一步了解风险" }, "learnMoreAboutAutofill": { - "message": "了解更多关于自动填充的信息" + "message": "进一步了解自动填充" }, "defaultAutoFillOnPageLoad": { "message": "登录项目的默认自动填充设置" @@ -1744,7 +1762,7 @@ "message": "州 / 省" }, "zipPostalCode": { - "message": "邮编 / 邮政代码" + "message": "邮政编码" }, "country": { "message": "国家" @@ -2318,12 +2336,15 @@ "message": "一个组织策略正影响您的所有权选项。" }, "personalOwnershipPolicyInEffectImports": { - "message": "组织策略已阻止将项目导入您的个人密码库。" + "message": "某个组织策略已阻止将项目导入您的个人密码库。" }, "domainsTitle": { "message": "域名", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "屏蔽的域名" + }, "excludedDomains": { "message": "排除域名" }, @@ -2331,7 +2352,16 @@ "message": "Bitwarden 将不会询问是否为这些域名保存登录信息。您必须刷新页面才能使更改生效。" }, "excludedDomainsDescAlt": { - "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。必须刷新页面才能使更改生效。" + "message": "Bitwarden 将不会询问是否为所有已登录账户的这些域名保存登录信息。您必须刷新页面才能使更改生效。" + }, + "blockedDomainsDesc": { + "message": "将不会为这些网站提供自动填充和其他相关功能。您必须刷新页面才能使更改生效。" + }, + "autofillBlockedNoticeV2": { + "message": "该网站的自动填充已被屏蔽。" + }, + "autofillBlockedNoticeGuidance": { + "message": "在设置中更改它" }, "websiteItemLabel": { "message": "网站 $number$ (URI)", @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "屏蔽域名更改已保存" + }, "excludedDomainsSavedSuccess": { "message": "排除域名更改已保存" }, @@ -2601,7 +2634,7 @@ "message": "更新主密码" }, "updateMasterPasswordWarning": { - "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" + "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" @@ -2789,6 +2822,20 @@ "error": { "message": "错误" }, + "decryptionError": { + "message": "解密错误" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 无法解密下列密码库项目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "联系客户成功团队", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "以避免额外的数据丢失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "生成用户名" }, @@ -2810,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个字符生成强大的密码。", + "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": { @@ -2820,7 +2867,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个单词生成强大的密码短语。", + "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": { @@ -3396,38 +3443,6 @@ "message": "切换折叠", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "导入您的数据到 Bitwarden 吗?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "保护您的 LastPass 数据并导入到 Bitwarden 吗?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "保存为未加密的文件", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "导入到 Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "导入中...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "数据导入成功!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "导入时出错。检查控制台以获取详细信息。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "导入过程中遇到网络错误。", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "别名域" }, @@ -3926,7 +3941,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "将 Bitwarden 设置为您的默认密码管理器吗?", + "message": "将 Bitwarden 设置为默认密码管理器吗?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { @@ -3934,7 +3949,7 @@ "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "将 Bitwarden 设置为您的默认密码管理器", + "message": "将 Bitwarden 设置为默认密码管理器", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { @@ -3981,6 +3996,9 @@ "autofillSuggestions": { "message": "自动填充建议" }, + "itemSuggestions": { + "message": "建议的项目" + }, "autofillSuggestionsTip": { "message": "将此站点保存为登录项目以用于自动填充" }, @@ -3991,7 +4009,7 @@ "message": "没有搜索到匹配的项目" }, "clearFiltersOrTryAnother": { - "message": "清除筛选器或尝试另一个搜索词" + "message": "清除筛选或尝试另一个搜索词" }, "copyInfoTitle": { "message": "复制信息 - $ITEMNAME$", @@ -4557,17 +4575,11 @@ "textSends": { "message": "文本 Send" }, - "bitwardenNewLook": { - "message": "Bitwarden 拥有一个新的外观!" - }, - "bitwardenNewLookDesc": { - "message": "从密码库标签页自动填充和搜索比以往任何时候都更简单直观。来看看吧!" - }, "accountActions": { "message": "账户操作" }, "showNumberOfAutofillSuggestions": { - "message": "在扩展图标上显示自动填充建议的登录的数量" + "message": "在扩展图标上显示自动填充建议的登录数量" }, "showQuickCopyActions": { "message": "在密码库上显示快速复制操作" @@ -4641,6 +4653,33 @@ "noEditPermissions": { "message": "您没有编辑此项目的权限" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "生物识别解锁不可用,因为需要先使用 PIN 或密码解锁。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物识别解锁当前不可用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "生物识别解锁不可用,因为 Bitwarden 桌面 App 已关闭。" + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "生物识别解锁不可用,因为在 Bitwarden 桌面 App 中没有为 $EMAIL$ 启用生物识别解锁。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "由于某个未知的原因,生物识别解锁当前不可用。" + }, "authenticating": { "message": "正在验证" }, @@ -4819,7 +4858,7 @@ "message": "稍后提醒我" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?", + "message": "您可以正常访问您的电子邮箱 $EMAIL$ 吗?", "placeholders": { "email": { "content": "$1", @@ -4847,5 +4886,11 @@ }, "extraWide": { "message": "超宽" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "请更新您的桌面应用程序" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "要使用生物识别解锁,请更新您的桌面应用程序,或在桌面设置中禁用指纹解锁。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 4b215df6ba2..c76929cd5c4 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -20,7 +20,7 @@ "message": "建立帳戶" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "第一次使用 Bitwarden?" }, "logInWithPasskey": { "message": "使用密碼金鑰登入" @@ -29,7 +29,7 @@ "message": "使用單一登入" }, "welcomeBack": { - "message": "Welcome back" + "message": "歡迎回來" }, "setAStrongPassword": { "message": "設定一個強密碼" @@ -84,7 +84,7 @@ "message": "加入組織" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "加入 $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -120,7 +120,7 @@ "message": "複製密碼" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "複製密碼短語" }, "copyNote": { "message": "複製備註" @@ -153,13 +153,13 @@ "message": "複製駕照號碼" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "複製私密金鑰" }, "copyPublicKey": { - "message": "Copy public key" + "message": "複製公開金鑰" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "複製指紋" }, "copyCustomField": { "message": "複製 $FIELD$", @@ -177,7 +177,7 @@ "message": "複製備註" }, "fill": { - "message": "Fill", + "message": "填入", "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": "自動填入身分資訊" }, "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": { @@ -337,22 +337,22 @@ "message": "您可以使用 Bitwarden 驗證器儲存驗證器金鑰,並為兩步驟驗證流程產生 TOTP 代碼。前往 bitwarden.com 網站以了解更多資訊。" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Bitwarden 密鑰管理" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "使用 Bitwarden 密鑰管理來安全儲存、管理並分享開發人員密鑰。在 bitwarden.com 網站上了解更多。" }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "使用 Passwordless.dev 建立流暢且安全的登入體驗,無需使用傳統密碼。在 bitwarden 網站上了解更多。" }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "免費的 Bitwarden 家庭方案" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "您符合免費 Bitwarden 家庭方案的資格要求。在網頁應用程式中兌換您的優惠。" }, "version": { "message": "版本" @@ -379,16 +379,16 @@ "message": "資料夾名稱" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "在資料夾名稱後面使用「/」來建立樹狀結構。\n例如:社交網路/論壇" }, "noFoldersAdded": { - "message": "No folders added" + "message": "未新增資料夾" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "建立資料夾來管理您的密碼庫項目" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "您確定要永久刪除此資料夾嗎?" }, "deleteFolder": { "message": "刪除資料夾" @@ -431,7 +431,7 @@ "message": "自動產生安全、唯一的登入密碼。" }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden 網頁應用程式" }, "importItems": { "message": "匯入項目" @@ -443,7 +443,19 @@ "message": "產生密碼" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "產生密碼短語" + }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" }, "regeneratePassword": { "message": "重新產生密碼" @@ -475,7 +487,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "包含大寫字元", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -483,7 +495,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "包含小寫字元", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -491,7 +503,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "包含數字", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -499,7 +511,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "包含特殊字元", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -526,11 +538,11 @@ "message": "最少符號位數" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "避免易混淆的字元", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "企業原則之要求已在你的產生器選項中生效", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -564,19 +576,19 @@ "message": "我的最愛" }, "unfavorite": { - "message": "Unfavorite" + "message": "取消最愛" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "項目已加入到最愛" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "項目已從最愛中移除" }, "notes": { "message": "備註" }, "privateNote": { - "message": "Private note" + "message": "私人備註" }, "note": { "message": "備註" @@ -600,7 +612,7 @@ "message": "開啟網站" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "開啟網站 $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -647,11 +659,17 @@ "verifyIdentity": { "message": "驗證身份" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "您的密碼庫已被鎖定" }, "yourAccountIsLocked": { "message": "您的帳戶已被鎖定。" @@ -742,7 +760,7 @@ "message": "主密碼" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "若您忘記主密碼,將會無法找回!" }, "masterPassHintLabel": { "message": "您已成功創建新帳戶!" @@ -782,7 +800,7 @@ "message": "您已成功創建新帳戶!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "你已經登入!" }, "youSuccessfullyLoggedIn": { "message": "登入成功" @@ -797,7 +815,7 @@ "message": "必須填入驗證碼。" }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "驗證已被取消或時間超過。請再試一次。" }, "invalidVerificationCode": { "message": "無效的驗證碼" @@ -834,7 +852,7 @@ "message": "Bitwarden 可以儲存並填入兩步驟驗證碼。選擇相機圖示來截取此網站的驗證器QR code,或手動複製金鑰並貼上到此欄位。" }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "了解更多驗證程式" }, "copyTOTP": { "message": "複製驗證器金鑰 (TOTP)" @@ -852,19 +870,19 @@ "message": "登入" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "登入 Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "重新啟動註冊" }, "expiredLink": { - "message": "Expired link" + "message": "過期連結" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "請重新啟動註冊流程或是重試登入。" }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "您可能已經有帳號" }, "logOutConfirmation": { "message": "您確定要登出嗎?" @@ -888,7 +906,7 @@ "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "在 Bitwarden 網頁應用程式中設定兩步驟登入,讓您的帳號更加安全。" }, "twoStepLoginConfirmationTitle": { "message": "前往網頁 App 嗎?" @@ -934,7 +952,7 @@ "message": "新增 URI" }, "addDomain": { - "message": "Add domain", + "message": "新增網域", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "如果在您的密碼庫中找不到項目,則詢問是否新增項目。適用於所有已登入的帳戶。" }, - "showCardsInVaultView": { - "message": "在密碼庫介面中顯示支付卡自動填入建議" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "於分頁頁面顯示支付卡" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "於分頁頁面顯示信用卡以便於自動填入。" }, - "showIdentitiesInVaultView": { - "message": "在密碼庫介面中顯示身分自動填入建議" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "於分頁頁面顯示身分" @@ -1005,7 +1023,7 @@ "message": "於分頁頁面顯示身分以便於自動填入。" }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "在密碼庫檢視中點擊項目來自動填入" }, "clearClipboard": { "message": "清除剪貼簿", @@ -1317,10 +1335,10 @@ "message": "輸入驗證器應用程式提供的 6 位數驗證碼。" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "驗證逾時" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "驗證工作階段因時間過久已逾時。請重試登入。" }, "enterVerificationCodeEmail": { "message": "輸入已傳送至 $EMAIL$ 的 6 位數驗證碼。", @@ -1386,17 +1404,17 @@ "message": "驗證器應用程式" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "輸入驗證器應用程式產生的驗證碼,例如 Bitwarden 驗證器。", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "YubiKey OTP 安全金鑰" }, "yubiKeyDesc": { "message": "使用 YubiKey 存取您的帳戶。支援 YubiKey 4、4 Nano、4C、以及 NEO 裝置。" }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "輸入 Duo 應用程式產生的驗證碼。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1413,7 +1431,7 @@ "message": "電子郵件" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "輸入寄送到您電子郵件信箱的驗證碼。" }, "selfHostedEnvironment": { "message": "自我裝載環境" @@ -1422,13 +1440,13 @@ "message": "指定您內部部署的 Bitwarden 安裝之基礎 URL。" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "指定您自建的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "您必須新增伺服器網域 URL 或至少一個自定義環境。" }, "customEnvironment": { "message": "自訂環境" @@ -1440,7 +1458,7 @@ "message": "伺服器 URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "自建伺服器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1466,22 +1484,22 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "自動填入建議" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "在表單欄位上顯示自動填入選單" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "顯示身分建議" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "顯示信用卡建議" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "當選擇圖示時,顯示建議" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "適用於所有已登入的帳戶。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "關閉你的瀏覽器內建密碼管理器設定以避免衝突。" @@ -1502,7 +1520,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "頁面載入時自動填入" }, "enableAutoFillOnPageLoad": { "message": "頁面載入時自動填入" @@ -1514,7 +1532,7 @@ "message": "被入侵或不被信任的網站,可能會濫用頁面載入的自動填入功能。" }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "了解更多風險" }, "learnMoreAboutAutofill": { "message": "進一步瞭解「自動填入」功能" @@ -1544,13 +1562,13 @@ "message": "在側邊欄中開啟密碼庫" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "自動將上次使用的登入資料填入目前網站" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "自動將上次使用的信用卡填入目前網站" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "自動將上次使用的身分資料填入目前網站" }, "commandGeneratePasswordDesc": { "message": "產生一組新的隨機密碼並將它複製到剪貼簿中。" @@ -1768,7 +1786,7 @@ "message": "身分" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH 金鑰" }, "newItemHeader": { "message": "新增 $TYPE$", @@ -1801,13 +1819,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": "若繼續,所有產生器曾經產生的記錄會被刪除。您確定要繼續?" }, "back": { "message": "返回" @@ -1846,7 +1864,7 @@ "message": "安全筆記" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH 金鑰" }, "clear": { "message": "清除", @@ -1929,10 +1947,10 @@ "message": "清除歷史紀錄" }, "nothingToShow": { - "message": "Nothing to show" + "message": "沒有可顯示的內容" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "您最近未產生任何密碼" }, "remove": { "message": "移除" @@ -2002,7 +2020,7 @@ "message": "設定您用來解鎖 Bitwarden 的 PIN 碼。您的 PIN 設定將在您完全登出本應用程式時被重設。" }, "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": "您的 PIN 碼會取代主密碼用來解鎖 Bitwarden。您的 PIN 碼會重置,若您完全登出 Bitwarden。" }, "pinRequired": { "message": "必須填入 PIN 碼。" @@ -2017,7 +2035,7 @@ "message": "使用生物特徵辨識解鎖" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "使用主密碼解鎖" }, "awaitDesktop": { "message": "等待來自桌面應用程式的確認" @@ -2029,7 +2047,7 @@ "message": "瀏覽器重啟後使用主密碼鎖定" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "瀏覽器重啟後使用主密碼鎖定" }, "selectOneCollection": { "message": "您必須至少選擇一個集合。" @@ -2041,26 +2059,26 @@ "message": "克隆" }, "passwordGenerator": { - "message": "Password generator" + "message": "密碼產生器" }, "usernameGenerator": { - "message": "Username generator" + "message": "使用者名稱產生器" }, "useThisPassword": { - "message": "Use this password" + "message": "使用此密碼" }, "useThisUsername": { - "message": "Use this username" + "message": "使用此使用者名稱" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "已產生安全的密碼!請不要忘記同時更新您網站上的密碼。" }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "使用產生器", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "來產生高強度且唯一的密碼", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { @@ -2096,7 +2114,7 @@ "message": "項目已還原" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "已經有帳號了嗎?" }, "vaultTimeoutLogOutConfirmation": { "message": "選擇登出將會在密碼庫逾時後移除對密碼庫的所有存取權限,若要重新驗證則需連線網路。確定要使用此設定嗎?" @@ -2108,7 +2126,7 @@ "message": "自動填入並儲存" }, "fillAndSave": { - "message": "Fill and save" + "message": "填入並儲存" }, "autoFillSuccessAndSavedUri": { "message": "項目已自動填入並且已儲存統一資源標識符(URI)" @@ -2189,19 +2207,19 @@ "message": "您新的主密碼不符合原則要求。" }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "獲得來自 Bitwarden 的公告、建議及研究資訊電子郵件。" }, "unsubscribe": { - "message": "Unsubscribe" + "message": "取消訂閱" }, "atAnyTime": { - "message": "at any time." + "message": "在任何時間。" }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "若是繼續,則代表您同意" }, "and": { - "message": "and" + "message": "和" }, "acceptPolicies": { "message": "選中此選取框,即表示您同意下列條款:" @@ -2222,10 +2240,10 @@ "message": "確定" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "存取權杖更新失敗" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "未找到存取權杖或 API 密鑰。請重試登出再登入。" }, "desktopSyncVerificationTitle": { "message": "桌面同步驗證" @@ -2264,10 +2282,10 @@ "message": "帳戶不相符" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "生物辨識金鑰錯誤" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "生物辨識解鎖失敗。生物辨識金鑰解鎖密碼庫失敗。請嘗試重新設定生物辨識。" }, "biometricsNotEnabledTitle": { "message": "生物特徵辨識未設定" @@ -2282,16 +2300,16 @@ "message": "此裝置不支援瀏覽器生物特徵辨識。" }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "使用者已鎖定或登出" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "請在桌面應用程式解鎖此使用者之後再重試。" }, "biometricsNotAvailableTitle": { "message": "生物辨識解鎖不可用" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "生物辨識解鎖現在無法使用。請稍後重試。" }, "biometricsFailedTitle": { "message": "生物特徵辨識失敗" @@ -2324,6 +2342,9 @@ "message": "網域", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "已封鎖的網域" + }, "excludedDomains": { "message": "排除網域" }, @@ -2333,6 +2354,15 @@ "excludedDomainsDescAlt": { "message": "對於所有已登入的帳戶,Bitwarden 不會詢問是否儲存這些網域的登入資訊。您必須重新整理頁面變更才會生效。" }, + "blockedDomainsDesc": { + "message": "自動填入及其它相關的功能無法在這些網站上使用。您必須重新整理頁面來使變更生效。" + }, + "autofillBlockedNoticeV2": { + "message": "自動填入已於此網站封鎖" + }, + "autofillBlockedNoticeGuidance": { + "message": "您可以於設定中進行更改" + }, "websiteItemLabel": { "message": "網站 $number$ (URI)", "placeholders": { @@ -2351,6 +2381,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "已儲存封鎖的網域" + }, "excludedDomainsSavedSuccess": { "message": "例外網域更改已儲存" }, @@ -2789,6 +2822,20 @@ "error": { "message": "錯誤" }, + "decryptionError": { + "message": "解密發生錯誤" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 無法解密您密碼庫中下面的項目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "聯絡客戶支援部門", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "來避免更多資料遺失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "產生使用者名稱" }, @@ -2887,7 +2934,7 @@ "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "網站:$WEBSITE$。透過 Bitwarden 產生。", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2897,7 +2944,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "無效的 $SERVICENAME$ API 權杖", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2907,7 +2954,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "無效的 $SERVICENAME$ API 權杖:$ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2921,7 +2968,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "無法獲得 $SERVICENAME$ 的轉送電子郵件帳號。", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2931,7 +2978,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "無效的 $SERVICENAME$ 網域。", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2941,7 +2988,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "無效的 $SERVICENAME$ URI。", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2951,7 +2998,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "發生未知的 $SERVICENAME$ 錯誤。", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2961,7 +3008,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "未知的轉送服務提供商:$SERVICENAME$。", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3068,25 +3115,25 @@ "message": "重新傳送通知" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "notificationSentDevice": { "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": "請確保您的帳號已解鎖,並且指紋短語與其他裝置一致。" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "一旦您的請求被通過,您會獲得通知。" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "需要另一個選項嗎?" }, "loginInitiated": { "message": "登入已啟動" @@ -3146,22 +3193,22 @@ "message": "自動填入設定" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "自動填入快速鍵" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "修改鍵盤快速鍵" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "管理鍵盤快速鍵" }, "autofillShortcut": { "message": "自動填入鍵盤快速鍵" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "自動填入快速鍵尚未設定。請在瀏覽器的設定中變更。" }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "自動填入快速鍵是 $COMMAND$。請在瀏覽器的設定中變更。", "placeholders": { "command": { "content": "$1", @@ -3182,16 +3229,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": "記住這個裝置" @@ -3212,25 +3259,25 @@ "message": "需要組織 SSO 識別碼。" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "建立帳號於" }, "checkYourEmail": { - "message": "Check your email" + "message": "檢查您的電子郵件" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "跟隨電子郵件中的連結" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "並繼續建立您的帳號" }, "noEmail": { - "message": "No email?" + "message": "沒有電子郵件?" }, "goBack": { - "message": "Go back" + "message": "返回" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "來編輯您的電子郵件位址。" }, "eu": { "message": "歐盟", @@ -3267,17 +3314,17 @@ "message": "缺少使用者電子郵件地址" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "未找到使用中帳號的電子郵件。正在將您登出。" }, "deviceTrusted": { "message": "裝置已信任" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "沒有可用的 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "使用 Send 可以與任何人安全地共用加密資訊。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3354,10 +3401,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "您需注意上方的 1 個欄位。" }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "您需注意上方的 $COUNT$ 個欄位。", "placeholders": { "count": { "content": "$1", @@ -3396,38 +3443,6 @@ "message": "切換至折疊狀態", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "匯入你的資料至 Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "保護你的 LastPass 資料並匯入至 Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "儲存為未加密的檔案", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "匯入至 Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "匯入中……", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "資料匯入成功!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "匯入時發生錯誤。檢查控制台以了解詳細資訊。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "匯入時遇到網路錯誤", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "別名網域" }, @@ -3444,7 +3459,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "切換側邊欄" }, "skipToContent": { "message": "跳至內容" @@ -3466,7 +3481,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "解鎖您的帳號來查看建議的自動填入", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3474,15 +3489,15 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "解鎖您的帳號,並開啟在新視窗", "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": "現在的 TOTP 到期剩餘時間", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3510,23 +3525,23 @@ "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "新增新的密碼庫登入項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "新信用卡", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "新增新的密碼庫信用卡項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "新身份識別", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "新增新的密碼庫身分項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -3616,19 +3631,19 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "連接到 Duo 服務時發生錯誤。使用不同的兩階段認證或聯繫 Duo 來獲得支援。" }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "您的帳號要求使用 Duo 兩步驟驗證登入。" }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "彈出擴充套件視窗來完成登入。" }, "popoutExtension": { - "message": "Popout extension" + "message": "彈出擴充套件視窗" }, "launchDuo": { "message": "開啟Duo" @@ -3646,7 +3661,7 @@ "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, "destination": { - "message": "Destination" + "message": "目的" }, "learnAboutImportOptions": { "message": "瞭解更多匯入選項" @@ -3705,16 +3720,16 @@ "message": "確認檔案密碼" }, "exportSuccess": { - "message": "Vault data exported" + "message": "密碼庫資料已匯出" }, "typePasskey": { "message": "密碼金鑰" }, "accessing": { - "message": "Accessing" + "message": "正在存取" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "已登入!" }, "passkeyNotCopied": { "message": "密碼金鑰不會被複製" @@ -3738,7 +3753,7 @@ "message": "您沒有符合該網站的登入資訊。" }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "未找到此網站的登入資訊" }, "searchSavePasskeyNewLogin": { "message": "搜尋或將密碼金鑰儲存為新的登入資訊" @@ -3877,19 +3892,19 @@ "message": "伺服器" }, "hostedAt": { - "message": "hosted at" + "message": "架設在" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "使用您的裝置或密碼金鑰" }, "justOnce": { "message": "僅此一次" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "永遠針對此網站" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "$DOMAIN$ 已新增到排除的網域。", "placeholders": { "domain": { "content": "$1", @@ -3898,51 +3913,51 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "繼續前往瀏覽器設定?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "繼續前往說明中心?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "在您瀏覽器的偏好設定中更改自動填入及密碼管理。", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "您可以在您瀏覽器的偏好設定中檢視及設定擴充套件的快速鍵。", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "在您瀏覽器的偏好設定中更改自動填入及密碼管理。", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "您可以在您瀏覽器的偏好設定中檢視及設定擴充套件的快速鍵。", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "使用 Bitwarden 作為預設的密碼管理器?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "忽略此設定可能會導致 Bitwarden 自動填入選單與您的瀏覽器產生衝突。", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "使用 Bitwarden 作為預設的密碼管理器", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "無法設定 Bitwarden 作為預設的密碼管理器", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "您必須同意您瀏覽器的隱私權限設定來設定 Bitwarden 為預設的密碼管理器。", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { @@ -3950,19 +3965,19 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "憑證資訊成功儲存!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "密碼已儲存!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "憑證資訊成功更新!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "密碼已更新!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3970,7 +3985,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "成功" }, "removePasskey": { "message": "移除密碼金鑰" @@ -3979,19 +3994,22 @@ "message": "密碼金鑰已移除" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "自動填入建議" + }, + "itemSuggestions": { + "message": "建議項目" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "對此網站儲存登入項目為自動填入" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "您的密碼庫是空的" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "沒有找到相符的項目。" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "清除過濾器或更換另一個搜尋條件" }, "copyInfoTitle": { "message": "複製資訊 - $ITEMNAME$", @@ -4004,7 +4022,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "複製備註 - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4034,7 +4052,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "檢視項目 - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4044,7 +4062,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "自動填入 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4054,22 +4072,22 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "沒有資料可以複製" }, "assignToCollections": { "message": "指派至集合" }, "copyEmail": { - "message": "Copy email" + "message": "複製電子郵件地址" }, "copyPhone": { - "message": "Copy phone" + "message": "複製電話" }, "copyAddress": { - "message": "Copy address" + "message": "複製地址" }, "adminConsole": { - "message": "Admin Console" + "message": "管理控制台" }, "accountSecurity": { "message": "帳戶安全性" @@ -4081,13 +4099,13 @@ "message": "外觀" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "指定目標集合時發生錯誤。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "指定目標資料夾時發生錯誤。" }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "檢視 $NAME$ 中的項目", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4097,7 +4115,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "回到 $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4107,7 +4125,7 @@ } }, "new": { - "message": "New" + "message": "新增" }, "removeItem": { "message": "移除 $NAME$", @@ -4120,16 +4138,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "不在任何資料夾中的項目" }, "itemDetails": { "message": "項目詳細資訊" }, "itemName": { - "message": "Item name" + "message": "項目名稱" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。", "placeholders": { "collections": { "content": "$1", @@ -4138,47 +4156,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "組織已被停用" }, "owner": { - "message": "Owner" + "message": "擁有者" }, "selfOwnershipLabel": { - "message": "You", + "message": "您", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "無法存取已停用組織中的項目。請聯絡您組織的擁有者以獲取協助。" }, "additionalInformation": { - "message": "Additional information" + "message": "更多資訊" }, "itemHistory": { - "message": "Item history" + "message": "項目歷史記錄" }, "lastEdited": { - "message": "Last edited" + "message": "最後編輯" }, "ownerYou": { - "message": "Owner: You" + "message": "擁有者: 您" }, "linked": { - "message": "Linked" + "message": "連結" }, "copySuccessful": { - "message": "Copy Successful" + "message": "複製成功" }, "upload": { - "message": "Upload" + "message": "上傳" }, "addAttachment": { - "message": "Add attachment" + "message": "新增附件" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "最大檔案大小為 500MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "刪除附檔 $NAME$", "placeholders": { "name": { "content": "$1", @@ -4187,7 +4205,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "下載 $NAME$", "placeholders": { "name": { "content": "$1", @@ -4196,25 +4214,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "您確定要永久刪除此附檔嗎?" }, "premium": { "message": "進階版" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "免費組織無法使用附檔" }, "filters": { - "message": "Filters" + "message": "篩選器" }, "filterVault": { - "message": "Filter vault" + "message": "過濾密碼庫" }, "filterApplied": { - "message": "One filter applied" + "message": "套用了一個過濾條件" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "套用了 $COUNT$ 個過濾條件", "placeholders": { "count": { "content": "$1", @@ -4226,13 +4244,13 @@ "message": "個人資訊" }, "identification": { - "message": "Identification" + "message": "身份" }, "contactInfo": { - "message": "Contact info" + "message": "聯繫資訊" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "下載 - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4241,23 +4259,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "以此號碼結尾的信用卡", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "登入資訊" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "驗證器金鑰" }, "autofillOptions": { "message": "自動填入選項" }, "websiteUri": { - "message": "Website (URI)" + "message": "網站 (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "網站 (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4267,16 +4285,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "網站已新增" }, "addWebsite": { - "message": "Add website" + "message": "新增網站" }, "deleteWebsite": { - "message": "Delete website" + "message": "刪除網站" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "預設 ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4286,7 +4304,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "顯示偵測到的吻合 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4295,7 +4313,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "隱藏偵測到的吻合 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4304,13 +4322,13 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "在頁面載入時自動填寫?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "過期的信用卡" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "如果您已續期,請更新信用卡資訊" }, "cardDetails": { "message": "信用卡詳細資料" @@ -4334,17 +4352,17 @@ "message": "新增帳戶" }, "loading": { - "message": "Loading" + "message": "正在載入" }, "data": { - "message": "Data" + "message": "資料" }, "passkeys": { "message": "密碼金鑰", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "密碼", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { @@ -4352,16 +4370,16 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "指定" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "只有可以檢視集合的組織成員才能看到其中的項目。" }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "只有可以檢視集合的組織成員才能看到其中的項目。" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "您已經選擇 $TOTAL_COUNT$ 個項目。由於您沒有編輯權限,無法更新其中的 $READONLY_COUNT$ 個項目。", "placeholders": { "total_count": { "content": "$1", @@ -4373,37 +4391,37 @@ } }, "addField": { - "message": "Add field" + "message": "新增欄位" }, "add": { - "message": "Add" + "message": "新增" }, "fieldType": { - "message": "Field type" + "message": "欄位類型" }, "fieldLabel": { - "message": "Field label" + "message": "欄位標籤" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "像是安全問答的資訊可以使用文字欄位來儲存" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "像是密碼的機密資訊可以使用隱藏欄位來儲存" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "若您想自動填入欄位中的核取方塊,例如儲存電子郵件,可以使用核取方塊。" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "使用連結欄位若您在特定網站上遇到自動填入問題。" }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "填入欄位的 html id、名稱、標籤或預留字元" }, "editField": { - "message": "Edit field" + "message": "編輯欄位" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "編輯 $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4412,7 +4430,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "刪除 $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4421,7 +4439,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ 已新增", "placeholders": { "label": { "content": "$1", @@ -4430,7 +4448,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "重新排序 $LABEL$。使用方向鍵來往上或下移動。", "placeholders": { "label": { "content": "$1", @@ -4439,7 +4457,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "往上移動 $LABEL$,位置 $LENGTH$ 之 $INDEX$", "placeholders": { "label": { "content": "$1", @@ -4459,10 +4477,10 @@ "message": "選擇要指派的集合" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1 個項目會被永久移到選擇的組織。您將不再擁有此項目。" }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 個項目會被永久移到選擇的組織。您將不再擁有這些項目。", "placeholders": { "personal_items_count": { "content": "$1", @@ -4471,7 +4489,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "1 個項目會被永久移到 $ORG$。您將不再擁有此項目。", "placeholders": { "org": { "content": "$1", @@ -4480,7 +4498,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 個項目會被永久移到 $ORG$。您將不再擁有這些項目。", "placeholders": { "personal_items_count": { "content": "$1", @@ -4496,10 +4514,10 @@ "message": "指派集合成功" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "您沒有選擇任何項目。" }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "將已選取項目移動至 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4508,7 +4526,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "項目已移到 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4517,7 +4535,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "項目已移到 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4526,7 +4544,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "往下移動 $LABEL$,位置 $LENGTH$ 之 $INDEX$", "placeholders": { "label": { "content": "$1", @@ -4543,52 +4561,46 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "項目位置" }, "fileSend": { - "message": "File Send" + "message": "檔案 Send" }, "fileSends": { - "message": "File Sends" + "message": "檔案 Send" }, "textSend": { - "message": "Text Send" + "message": "文字 Send" }, "textSends": { - "message": "Text Sends" - }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "文字 Sends" }, "accountActions": { - "message": "Account actions" + "message": "帳號動作" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "在擴充套件圖示上顯示自動填入建議的數量" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "在密碼庫中顯示快速複製" }, "systemDefault": { - "message": "System default" + "message": "系統預設值" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "企業政策已套用至您的選項中" }, "sshPrivateKey": { - "message": "Private key" + "message": "私密金鑰" }, "sshPublicKey": { - "message": "Public key" + "message": "公共金鑰" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "指紋" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "金鑰類型" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4603,223 +4615,250 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "重試" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "自訂逾時時間最小為 1 分鐘。" }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "以及更多內容" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "檔案已儲存到裝置。在您的裝置上管理下載檔案。" }, "showCharacterCount": { - "message": "Show character count" + "message": "顯示字元數" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "隱藏字元數" }, "itemsInTrash": { - "message": "Items in trash" + "message": "在垃圾桶中的項目" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "垃圾桶中沒有項目" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "您刪除的項目會在此顯示,並會在 30 天之後永久刪除" }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "垃圾桶中超過 30 天的項目將會被自動刪除。" }, "restore": { - "message": "Restore" + "message": "還原" }, "deleteForever": { - "message": "Delete forever" + "message": "永遠刪除" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "你沒有權限編輯這個項目" + }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "需要 PIN 碼或密碼解鎖才能使用生物辨識解鎖。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物辨識解鎖暫時無法使用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "由於 Bitwarden 桌面應用程式已關閉,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "由於未 Bitwarden 桌面應用程式的 $EMAIL$ 帳號上啟動,生物辨識解鎖無法使用。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "基於不明原因,生物辨識解鎖無法使用。" }, "authenticating": { - "message": "Authenticating" + "message": "驗證中" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "自動填入產生的密碼", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "密碼已重新產生", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "在 Bitwarden 中儲存登入資訊?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "空格", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "波浪", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "重音符", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "驚歎號", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "在符號", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "井字號", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "錢字號", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "百分比", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "插入號", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "和符號", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "星號", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "左括號", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "右括號", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "底線", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "連字號", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "加號", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "等號", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "左大括號", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "右大括號", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "左中括號", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "右中括號", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "垂直符號", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "反斜線", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "冒號", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "分號", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "雙引號", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "單引號", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "小於", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "大於", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "逗號", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "句號", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "問號", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "斜線", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "小寫" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "大寫" }, "generatedPassword": { - "message": "Generated password" + "message": "已產生的密碼" }, "compactMode": { - "message": "Compact mode" + "message": "緊湊模式" }, "beta": { - "message": "Beta" + "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": "從 2025 年 2 月開始,Bitwarden 會傳送代碼到您的帳號電子郵件中來驗證新裝置的登入。" }, "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,24 +4867,30 @@ } }, "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": "Extension width" + "message": "擴充套件寬度" }, "wide": { - "message": "Wide" + "message": "寬度" }, "extraWide": { - "message": "Extra wide" + "message": "更寬" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "請更新您的桌面應用程式" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "為了使用生物辨識解鎖,請更新您的桌面應用程式,或在設定中停用指紋解鎖。" } } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index ad7e6f67361..841eefda0ad 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -25,6 +25,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, I18nMockService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index ed395797961..08043cf88bb 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -1,4 +1,4 @@ - +
diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 6060ac77abe..dbdcf7d5aa8 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -6,7 +6,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -30,9 +30,6 @@ export class HomeComponent implements OnInit, OnDestroy { rememberEmail: [false], }); - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - constructor( protected platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, @@ -40,7 +37,6 @@ export class HomeComponent implements OnInit, OnDestroy { private i18nService: I18nService, private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, - private registerRouteService: RegisterRouteService, private toastService: ToastService, private configService: ConfigService, private route: ActivatedRoute, @@ -118,13 +114,15 @@ export class HomeComponent implements OnInit, OnDestroy { } await this.setLoginEmailValues(); - await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); + await this.router.navigate(["login"], { + queryParams: { email: this.formGroup.controls.email.value }, + }); } async setLoginEmailValues() { // Note: Browser saves email settings here instead of the login component - this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); - await this.loginEmailService.setLoginEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.controls.rememberEmail.value); + await this.loginEmailService.setLoginEmail(this.formGroup.controls.email.value); await this.loginEmailService.saveEmailSettings(); } } diff --git a/apps/browser/src/auth/popup/login-v1.component.ts b/apps/browser/src/auth/popup/login-v1.component.ts index cb6380d62ed..b2c52f248c6 100644 --- a/apps/browser/src/auth/popup/login-v1.component.ts +++ b/apps/browser/src/auth/popup/login-v1.component.ts @@ -10,11 +10,9 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, - RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -51,8 +49,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - webAuthnLoginService: WebAuthnLoginServiceAbstraction, - registerRouteService: RegisterRouteService, toastService: ToastService, ) { super( @@ -73,8 +69,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { route, loginEmailService, ssoLoginService, - webAuthnLoginService, - registerRouteService, toastService, ); this.onSuccessfulLogin = async () => { diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index 3f874fc1a76..8bc28c9754d 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -20,7 +20,7 @@

{{ "unlockMethods" | i18n }}

{{ biometricUnavailabilityReason }} - + {{ "otherOptions" | i18n }} - - - {{ "lockNow" | i18n }} - - - - {{ "logOut" | i18n }} - diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 099f445be85..5979afca4b8 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -29,6 +29,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli 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 { DeviceType } from "@bitwarden/common/enums"; 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"; @@ -63,7 +64,6 @@ import { 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 { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -107,7 +107,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { hasVaultTimeoutPolicy = false; biometricUnavailabilityReason: string; showChangeMasterPass = true; - accountSwitcherEnabled = false; + showAutoPrompt = true; form = this.formBuilder.group({ vaultTimeout: [null as VaultTimeout | null], @@ -140,11 +140,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private toastService: ToastService, private biometricsService: BiometricsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } + ) {} async ngOnInit() { + // Firefox popup closes when unfocused by biometrics, blocking all unlock methods + if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) { + this.showAutoPrompt = false; + } + const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); this.showMasterPasswordOnClientRestartOption = hasMasterPassword; const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout); @@ -210,6 +213,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { switchMap(async () => { const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id); const biometricSettingAvailable = + !(await BrowserApi.permissionsGranted(["nativeMessaging"])) || (status !== BiometricsStatus.DesktopDisconnected && status !== BiometricsStatus.NotEnabledInConnectedDesktopApp) || (await this.vaultTimeoutSettingsService.isBiometricLockSet()); @@ -510,7 +514,18 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const biometricsPromise = async () => { try { - const result = await this.biometricsService.authenticateWithBiometrics(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a.id)), + ); + let result = false; + try { + const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId); + result = await this.keyService.validateUserKey(userKey, userId); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + result = false; + } // prevent duplicate dialog biometricsResponseReceived = true; diff --git a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts index 0917b2703cf..687dc683929 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts @@ -13,11 +13,20 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; @@ -38,7 +47,6 @@ import { ZonedMessageListenerService } from "../../platform/browser/zoned-messag AsyncActionsModule, FormsModule, ], - providers: [I18nPipe], }) export class TwoFactorAuthDuoComponent extends TwoFactorAuthDuoBaseComponent diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts index b6211bba05f..7afe1eb889e 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -6,12 +6,23 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DialogService } from "../../../../../libs/components/src/dialog"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; @@ -32,7 +43,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; AsyncActionsModule, FormsModule, ], - providers: [I18nPipe], }) export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent implements OnInit { private dialogService = inject(DialogService); diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 3cb82118597..f22bbbe202c 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -37,6 +37,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index a2f9cd9d0fc..43230bd23f4 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -33,7 +33,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; -import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; +import { closeTwoFactorAuthWebAuthnPopout } from "./utils/auth-popout-window"; @Component({ selector: "app-two-factor", @@ -171,7 +171,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // We don't need this window anymore because the intent is for the user to be left // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) - await closeTwoFactorAuthPopout(); + await closeTwoFactorAuthWebAuthnPopout(); }; } }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts index 9e7d69fad97..deb71f73cd6 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts @@ -7,8 +7,9 @@ import { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, + closeSsoAuthResultPopout, } from "./auth-popout-window"; describe("AuthPopoutWindow", () => { @@ -97,22 +98,30 @@ describe("AuthPopoutWindow", () => { }); }); - describe("openTwoFactorAuthPopout", () => { - it("opens a window that facilitates two factor authentication", async () => { - await openTwoFactorAuthPopout({ data: "data", remember: "remember" }); + describe("closeSsoAuthResultPopout", () => { + it("closes the SSO authentication result popout window", async () => { + await closeSsoAuthResultPopout(); + + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.ssoAuthResult); + }); + }); + + describe("openTwoFactorAuthWebAuthnPopout", () => { + it("opens a window that facilitates two factor authentication via WebAuthn", async () => { + await openTwoFactorAuthWebAuthnPopout({ data: "data", remember: "remember" }); expect(openPopoutSpy).toHaveBeenCalledWith( "popup/index.html#/2fa;webAuthnResponse=data;remember=remember", - { singleActionKey: AuthPopoutType.twoFactorAuth }, + { singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn }, ); }); }); - describe("closeTwoFactorAuthPopout", () => { - it("closes the two-factor authentication window", async () => { - await closeTwoFactorAuthPopout(); + describe("closeTwoFactorAuthWebAuthnPopout", () => { + it("closes the two-factor authentication WebAuthn window", async () => { + await closeTwoFactorAuthWebAuthnPopout(); - expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuth); + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuthWebAuthn); }); }); }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.ts index 5a0e577807f..8d6e7fa92cd 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.ts @@ -6,7 +6,7 @@ import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; const AuthPopoutType = { unlockExtension: "auth_unlockExtension", ssoAuthResult: "auth_ssoAuthResult", - twoFactorAuth: "auth_twoFactorAuth", + twoFactorAuthWebAuthn: "auth_twoFactorAuthWebAuthn", } as const; const extensionUnlockUrls = new Set([ chrome.runtime.getURL("popup/index.html#/lock"), @@ -60,26 +60,37 @@ async function openSsoAuthResultPopout(resultData: { code: string; state: string } /** - * Opens a window that facilitates two-factor authentication. + * Closes the SSO authentication result popout window. + */ +async function closeSsoAuthResultPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.ssoAuthResult); +} + +/** + * Opens a popout that facilitates two-factor authentication via WebAuthn. * - * @param twoFactorAuthData - The data from the two-factor authentication. + * @param twoFactorAuthWebAuthnData - The data to send ot the popout via query param. + * It includes the WebAuthn response and whether to save the 2FA remember me token or not. */ -async function openTwoFactorAuthPopout(twoFactorAuthData: { data: string; remember: string }) { - const { data, remember } = twoFactorAuthData; +async function openTwoFactorAuthWebAuthnPopout(twoFactorAuthWebAuthnData: { + data: string; + remember: string; +}) { + const { data, remember } = twoFactorAuthWebAuthnData; const params = `webAuthnResponse=${encodeURIComponent(data)};` + `remember=${encodeURIComponent(remember)}`; const twoFactorUrl = `popup/index.html#/2fa;${params}`; await BrowserPopupUtils.openPopout(twoFactorUrl, { - singleActionKey: AuthPopoutType.twoFactorAuth, + singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn, }); } /** * Closes the two-factor authentication popout window. */ -async function closeTwoFactorAuthPopout() { - await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuth); +async function closeTwoFactorAuthWebAuthnPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuthWebAuthn); } export { @@ -87,6 +98,7 @@ export { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + closeSsoAuthResultPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, }; diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ed9d8e6d84b..1b989283112 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -31,16 +31,10 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { type: "unlock"; } -interface AddRequestFilelessImportQueueMessage extends NotificationQueueMessage { - type: "fileless-import"; - importType?: string; -} - type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordQueueMessage - | AddUnlockVaultQueueMessage - | AddRequestFilelessImportQueueMessage; + | AddUnlockVaultQueueMessage; type LockedVaultPendingNotificationsData = { commandToRetry: { @@ -122,7 +116,6 @@ export { AddChangePasswordQueueMessage, AddLoginQueueMessage, AddUnlockVaultQueueMessage, - AddRequestFilelessImportQueueMessage, NotificationQueueMessageItem, LockedVaultPendingNotificationsData, AdjustNotificationBarMessageData, diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 03284f3fd89..6ad9b8e06fd 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -57,6 +57,17 @@ export type InlineMenuElementPosition = { height: number; }; +export type FieldRect = { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; + x: number; + y: number; +}; + export type InlineMenuPosition = { button?: InlineMenuElementPosition; list?: InlineMenuElementPosition; @@ -134,6 +145,7 @@ export type OverlayBackgroundExtensionMessage = { isFieldCurrentlyFilling?: boolean; subFrameData?: SubFrameOffsetData; focusedFieldData?: FocusedFieldData; + allFieldsRect?: any; isOpeningFullInlineMenu?: boolean; styles?: Partial; data?: LockedVaultPendingNotificationsData; diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts index 73f936bb591..a300ac08660 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -453,12 +453,16 @@ describe("AutoSubmitLoginBackground", () => { sendMockExtensionMessage({ command: "triggerAutoSubmitLogin" }, sender); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; }); it("skips acting on messages whose command does not have a registered handler", () => { sendMockExtensionMessage({ command: "someInvalidCommand" }, sender); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 5c6ff3c2c8c..0175b27bd69 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -37,7 +37,6 @@ import { AutofillService } from "../services/abstractions/autofill.service"; import { AddChangePasswordQueueMessage, AddLoginQueueMessage, - AddRequestFilelessImportQueueMessage, AddUnlockVaultQueueMessage, ChangePasswordMessageData, AddLoginMessageData, @@ -201,11 +200,6 @@ export default class NotificationBackground { case NotificationQueueMessageType.AddLogin: typeData.removeIndividualVault = await this.removeIndividualVault(); break; - case NotificationQueueMessageType.RequestFilelessImport: - typeData.importType = ( - notificationQueueMessage as AddRequestFilelessImportQueueMessage - ).importType; - break; } await BrowserApi.tabSendMessageData(tab, "openNotificationBar", { @@ -399,25 +393,6 @@ export default class NotificationBackground { } } - /** - * Sets up a notification to request a fileless import when the user - * attempts to trigger an import from a third party website. - * - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.getAuthStatus(); - if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { - return; - } - - const loginDomain = Utils.getDomain(tab.url); - if (loginDomain) { - await this.pushRequestFilelessImportToQueue(loginDomain, tab, importType); - } - } - private async pushChangePasswordToQueue( cipherId: string, loginDomain: string, @@ -456,36 +431,6 @@ export default class NotificationBackground { await this.sendNotificationQueueMessage(tab, message); } - /** - * Pushes a request to start a fileless import to the notification queue. - * This will display a notification bar to the user, prompting them to - * start the import. - * - * @param loginDomain - The domain of the tab that we are sending the notification to - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - private async pushRequestFilelessImportToQueue( - loginDomain: string, - tab: chrome.tabs.Tab, - importType?: string, - ) { - this.removeTabFromNotificationQueue(tab); - const launchTimestamp = new Date().getTime(); - const message: AddRequestFilelessImportQueueMessage = { - type: NotificationQueueMessageType.RequestFilelessImport, - domain: loginDomain, - tab, - launchTimestamp, - expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds - wasVaultLocked: false, - importType, - }; - this.notificationQueue.push(message); - await this.checkNotificationQueue(tab); - this.removeTabFromNotificationQueue(tab); - } - /** * Saves a cipher based on the message sent from the notification bar. If the vault * is locked, the message will be added to the notification queue and the unlock diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 0ac69317855..c3a6357ed05 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -2913,6 +2913,124 @@ describe("OverlayBackground", () => { ); }); }); + describe("handles menu position when input is focused", () => { + it("sets button and menu width and position when non-multi-input totp field is focused", async () => { + const subframe = { + top: 0, + left: 0, + url: "", + frameId: 0, + }; + + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + focusedFieldRects: { + width: 49.328125, + height: 64, + top: 302.171875, + left: 1270.8125, + }, + }); + + const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe); + const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe); + + expect(menuPostion).toEqual({ + width: "49px", + top: "366px", + left: "1271px", + }); + expect(buttonPostion).toEqual({ + width: "34px", + height: "34px", + top: "317px", + left: "1271px", + }); + }); + it("sets button and menu width and position when multi-input totp field is focused", async () => { + const subframe = { + top: 0, + left: 0, + url: "", + frameId: 0, + }; + + const totpFields = [ + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__0" }), + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__1" }), + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__2" }), + ]; + const allFieldData = [ + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__0", + rect: { + x: 1041.5, + y: 302.171875, + width: 49.328125, + height: 64, + top: 302.171875, + right: 1090.828125, + bottom: 366.171875, + left: 1041.5, + }, + }), + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__1", + rect: { + x: 1098.828125, + y: 302.171875, + width: 49.328125, + height: 64, + top: 302.171875, + right: 1148.15625, + bottom: 366.171875, + left: 1098.828125, + }, + }), + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__2", + rect: { + x: 1156.15625, + y: 302.171875, + width: 249.328125, + height: 64, + top: 302.171875, + right: 2205.484375, + bottom: 366.171875, + left: 2156.15625, + }, + }), + ]; + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + focusedFieldRects: { + width: 49.328125, + height: 64, + top: 302.171875, + left: 1270.8125, + }, + }); + + overlayBackground["allFieldData"] = allFieldData; + jest.spyOn(overlayBackground as any, "isTotpFieldForCurrentField").mockReturnValue(true); + jest.spyOn(overlayBackground as any, "getTotpFields").mockReturnValue(totpFields); + + const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe); + const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe); + expect(menuPostion).toEqual({ + width: "1164px", + top: "366px", + left: "1042px", + }); + expect(buttonPostion).toEqual({ + width: "34px", + height: "34px", + top: "292px", + left: "2187px", + }); + }); + }); describe("triggerDelayedAutofillInlineMenuClosure message handler", () => { it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 58e462943bf..3d2b1ec783c 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -70,6 +70,7 @@ import { generateDomainMatchPatterns, generateRandomChars, isInvalidResponseStatusCode, + rectHasSize, specialCharacterToKeyMap, } from "../utils"; @@ -130,6 +131,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private currentInlineMenuCiphersCount: number = 0; private currentAddNewItemData: CurrentAddNewItemData; private focusedFieldData: FocusedFieldData; + private allFieldData: AutofillField[]; private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFilling: boolean = false; private isInlineMenuButtonVisible: boolean = false; @@ -1367,6 +1369,71 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.isInlineMenuListVisible = false; } + /** + * Get all the totp fields for the tab and frame of the currently focused field + */ + private getTotpFields(): AutofillField[] { + const currentTabId = this.focusedFieldData?.tabId; + const currentFrameId = this.focusedFieldData?.frameId; + const pageDetailsMap = this.pageDetailsForTab[currentTabId]; + const pageDetails = pageDetailsMap?.get(currentFrameId); + + const fields = pageDetails.details.fields; + const totpFields = fields.filter((f) => + this.inlineMenuFieldQualificationService.isTotpField(f), + ); + + return totpFields; + } + + /** + * calculates the postion and width for multi-input totp field inline menu + * @param totpFieldArray - the totp fields used to evaluate the position of the menu + */ + private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) { + // Filter the fields based on the provided totpfields + const filteredObjects = this.allFieldData.filter((obj) => + totpFieldArray.some((o) => o.opid === obj.opid), + ); + + // Return null if no matching objects are found + if (filteredObjects.length === 0) { + return null; + } + // Calculate the smallest left and largest right values to determine width + const left = Math.min( + ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.left), + ); + const largestRight = Math.max( + ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.right), + ); + + const width = largestRight - left; + + return { left, width }; + } + + /** + * calculates the postion for multi-input totp field inline button + * @param totpFieldArray - the totp fields used to evaluate the position of the menu + */ + private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) { + const filteredObjects = this.allFieldData.filter((obj) => + totpFieldArray.some((o) => o.opid === obj.opid), + ); + + if (filteredObjects.length === 0) { + return null; + } + + const maxRight = Math.max(...filteredObjects.map((obj) => obj.rect.right)); + const maxObject = filteredObjects.find((obj) => obj.rect.right === maxRight); + const top = maxObject.rect.top - maxObject.rect.height * 0.39; + const left = maxRight - maxObject.rect.height * 0.3; + + return { left, top }; + } + /** * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. @@ -1472,8 +1539,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameTopOffset = subFrameOffsets?.top || 0; const subFrameLeftOffset = subFrameOffsets?.left || 0; - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { width, height } = this.focusedFieldData.focusedFieldRects; + let { top, left } = this.focusedFieldData.focusedFieldRects; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; + + if (this.isTotpFieldForCurrentField()) { + const totpFields = this.getTotpFields(); + if (totpFields.length > 1) { + ({ left, top } = this.calculateTotpMultiInputButtonBounds(totpFields)); + } + } + let elementOffset = height * 0.37; if (height >= 35) { elementOffset = height >= 50 ? height * 0.47 : height * 0.42; @@ -1512,7 +1588,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameTopOffset = subFrameOffsets?.top || 0; const subFrameLeftOffset = subFrameOffsets?.left || 0; - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { top, height } = this.focusedFieldData.focusedFieldRects; + let { left, width } = this.focusedFieldData.focusedFieldRects; + + if (this.isTotpFieldForCurrentField()) { + const totpFields = this.getTotpFields(); + + if (totpFields.length > 1) { + ({ left, width } = this.calculateTotpMultiInputMenuBounds(totpFields)); + } + } this.inlineMenuPosition.list = { top: Math.round(top + height + subFrameTopOffset), @@ -1535,7 +1620,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the extension message */ private setFocusedFieldData( - { focusedFieldData }: OverlayBackgroundExtensionMessage, + { focusedFieldData, allFieldsRect }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { if ( @@ -1552,6 +1637,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const previousFocusedFieldData = this.focusedFieldData; this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; + this.allFieldData = allFieldsRect; this.isFieldCurrentlyFocused = true; if (this.shouldUpdatePasswordGeneratorMenuOnFieldFocus()) { diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts index ff1dbd4e945..d70fc1e7446 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.spec.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -46,6 +46,8 @@ describe("AutoSubmitLogin content script", () => { beforeEach(() => { jest.useFakeTimers(); setupEnvironmentDefaults(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./auto-submit-login"); }); diff --git a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts new file mode 100644 index 00000000000..9e2da59d992 --- /dev/null +++ b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts @@ -0,0 +1,67 @@ +import { dirname, join } from "path"; +import path from "path"; +import type { StorybookConfig } from "@storybook/web-components-webpack5"; +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; +import remarkGfm from "remark-gfm"; + +const getAbsolutePath = (value: string): string => + dirname(require.resolve(join(value, "package.json"))); + +const config: StorybookConfig = { + stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"], + addons: [ + getAbsolutePath("@storybook/addon-links"), + getAbsolutePath("@storybook/addon-essentials"), + getAbsolutePath("@storybook/addon-a11y"), + getAbsolutePath("@storybook/addon-designs"), + getAbsolutePath("@storybook/addon-interactions"), + { + name: "@storybook/addon-docs", + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm], + }, + }, + }, + }, + ], + framework: { + name: getAbsolutePath("@storybook/web-components-webpack5"), + options: { + legacyRootApi: true, + }, + }, + core: { + disableTelemetry: true, + }, + env: (existingConfig) => ({ + ...existingConfig, + FLAGS: JSON.stringify({}), + }), + webpackFinal: async (config) => { + if (config.resolve) { + config.resolve.plugins = [ + new TsconfigPathsPlugin({ + configFile: path.resolve(__dirname, "../../../../../tsconfig.json"), + }), + ] as any; + } + + if (config.module && config.module.rules) { + config.module.rules.push({ + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve("ts-loader"), + }, + ], + }); + } + return config; + }, + docs: {}, +}; + +export default config; diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts index 695cbfd3b9d..cacd2b59f0e 100644 --- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -23,6 +23,8 @@ export function EditButton({ title=${buttonText} class=${editButtonStyles({ disabled, theme })} @click=${(event: Event) => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions !disabled && buttonAction(event); }} > diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts index 24f528c5246..acdee756570 100644 --- a/apps/browser/src/autofill/content/components/cipher/types.ts +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -1,3 +1,5 @@ +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const CipherTypes = { Login: 1, SecureNote: 2, @@ -7,6 +9,8 @@ const CipherTypes = { type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes]; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const CipherRepromptTypes = { None: 0, Password: 1, diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts new file mode 100644 index 00000000000..4e0efd5955c --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ActionButton } from "../../buttons/action-button"; + +type Args = { + buttonText: string; + disabled: boolean; + theme: Theme; + buttonAction: (e: Event) => void; +}; + +export default { + title: "Components/Buttons/Action Button", + argTypes: { + buttonText: { control: "text" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + buttonAction: { control: false }, + }, + args: { + buttonText: "Click Me", + disabled: false, + theme: ThemeTypes.Light, + buttonAction: () => alert("Clicked"), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=487-14755&t=2O7uCAkwRZCcjumm-4", + }, + }, +} as Meta; + +const Template = (args: Args) => ActionButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts new file mode 100644 index 00000000000..df44a70278d --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { BadgeButton } from "../../buttons/badge-button"; + +type Args = { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}; + +export default { + title: "Components/Buttons/Badge Button", + argTypes: { + buttonText: { control: "text" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + buttonAction: { control: false }, + }, + args: { + buttonText: "Click Me", + disabled: false, + theme: ThemeTypes.Light, + buttonAction: () => alert("Clicked"), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=502-24973&t=2O7uCAkwRZCcjumm-4", + }, + }, +} as Meta; + +const Template = (args: Args) => BadgeButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts new file mode 100644 index 00000000000..f622ab8b3ec --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts @@ -0,0 +1,35 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { CloseButton } from "../../buttons/close-button"; + +type Args = { + handleCloseNotification: (e: Event) => void; + theme: Theme; +}; +export default { + title: "Components/Buttons/Close Button", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + handleCloseNotification: { control: false }, + }, + args: { + theme: ThemeTypes.Light, + handleCloseNotification: () => { + alert("Close button clicked!"); + }, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=502-24633&t=2O7uCAkwRZCcjumm-4", + }, + }, +} as Meta; + +const Template = (args: Args) => CloseButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts new file mode 100644 index 00000000000..34ecae30dc2 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts @@ -0,0 +1,39 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { EditButton } from "../../buttons/edit-button"; + +type Args = { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}; +export default { + title: "Components/Buttons/Edit Button", + argTypes: { + buttonText: { control: "text" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + buttonAction: { control: false }, + }, + args: { + buttonText: "Click Me", + disabled: false, + theme: ThemeTypes.Light, + buttonAction: () => alert("Clicked"), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=502-24633&t=2O7uCAkwRZCcjumm-4", + }, + }, +} as Meta; + +const Template = (args: Args) => EditButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts new file mode 100644 index 00000000000..e597cddabe6 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; +import { CipherAction } from "../../cipher/cipher-action"; + +type Args = { + handleAction?: (e: Event) => void; + notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; + theme: Theme; +}; +export default { + title: "Components/Ciphers/Cipher Action", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + notificationType: { + control: "select", + options: [NotificationTypes.Change, NotificationTypes.Add], + }, + handleAction: { control: false }, + }, + args: { + theme: ThemeTypes.Light, + notificationType: NotificationTypes.Change, + handleAction: () => { + alert("Action triggered!"); + }, + }, +} as Meta; + +const Template = (args: Args) => CipherAction({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts new file mode 100644 index 00000000000..a8884f063de --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { CipherIcon } from "../../cipher/cipher-icon"; + +type Args = { + color: string; + size: string; + theme: Theme; + uri?: string; +}; + +export default { + title: "Components/Ciphers/Cipher Icon", + argTypes: { + color: { control: "color" }, + size: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + uri: { control: "text" }, + }, + args: { + size: "50px", + theme: ThemeTypes.Light, + uri: "", + }, +} as Meta; + +const Template = (args: Args) => { + return html` +
+ ${CipherIcon({ ...args })} +
+ `; +}; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts new file mode 100644 index 00000000000..2d031fa3afd --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons"; + +type Args = { + isBusinessOrg?: boolean; + isFamilyOrg?: boolean; + theme: Theme; +}; + +export default { + title: "Components/Ciphers/Cipher Indicator Icon", + argTypes: { + isBusinessOrg: { control: "boolean" }, + isFamilyOrg: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + theme: ThemeTypes.Light, + isBusinessOrg: true, + isFamilyOrg: false, + }, +} as Meta; + +const Template: StoryObj["render"] = (args) => + html`
${CipherInfoIndicatorIcons({ ...args })}
`; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts new file mode 100644 index 00000000000..20c88a59246 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -0,0 +1,66 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import * as Icons from "../../icons"; + +type Args = { + color?: string; + disabled?: boolean; + theme: Theme; + size: number; + iconLink: URL; +}; + +export default { + title: "Components/Icons/Icons", + argTypes: { + iconLink: { control: "text" }, + color: { control: "color" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + size: { control: "number", min: 10, max: 100, step: 1 }, + }, + args: { + iconLink: new URL("https://bitwarden.com"), + disabled: false, + theme: ThemeTypes.Light, + size: 50, + }, +} as Meta; + +const Template = (args: Args, IconComponent: (props: Args) => ReturnType) => html` +
+ ${IconComponent({ ...args })} +
+`; + +const createIconStory = (iconName: keyof typeof Icons): StoryObj => { + const story = { + render: (args) => Template(args, Icons[iconName]), + } as StoryObj; + + if (iconName !== "BrandIconContainer") { + story.argTypes = { + iconLink: { table: { disable: true } }, + }; + } + + return story; +}; + +export const AngleDownIcon = createIconStory("AngleDown"); +export const BusinessIcon = createIconStory("Business"); +export const BrandIcon = createIconStory("BrandIconContainer"); +export const CloseIcon = createIconStory("Close"); +export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); +export const FamilyIcon = createIconStory("Family"); +export const FolderIcon = createIconStory("Folder"); +export const GlobeIcon = createIconStory("Globe"); +export const PartyHornIcon = createIconStory("PartyHorn"); +export const PencilSquareIcon = createIconStory("PencilSquare"); +export const ShieldIcon = createIconStory("Shield"); +export const UserIcon = createIconStory("User"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts new file mode 100644 index 00000000000..90616647b0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -0,0 +1,59 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { NotificationType } from "../../../../notification/abstractions/notification-bar"; +import { CipherData } from "../../cipher/types"; +import { NotificationBody } from "../../notification/body"; + +type Args = { + ciphers: CipherData[]; + notificationType: NotificationType; + theme: Theme; +}; + +export default { + title: "Components/Notifications/Notification Body", + argTypes: { + ciphers: { control: "object" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + notificationType: { + control: "select", + options: ["add", "change", "unlock", "fileless-import"], + }, + }, + args: { + ciphers: [ + { + id: "1", + name: "Example Cipher", + type: CipherType.Login, + favorite: false, + reprompt: CipherRepromptType.None, + icon: { + imageEnabled: true, + image: "", + fallbackImage: "https://example.com/fallback.png", + icon: "icon-class", + }, + login: { username: "user@example.com", passkey: null }, + }, + ], + theme: ThemeTypes.Light, + notificationType: "change", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=217-6841&m=dev", + }, + }, +} as Meta; + +const Template = (args: Args) => NotificationBody({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts new file mode 100644 index 00000000000..d63c72bf019 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts @@ -0,0 +1,38 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { NotificationType } from "../../../../notification/abstractions/notification-bar"; +import { NotificationFooter } from "../../notification/footer"; + +type Args = { + notificationType: NotificationType; + theme: Theme; +}; + +export default { + title: "Components/Notifications/Notification Footer", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + notificationType: { + control: "select", + options: ["add", "change", "unlock", "fileless-import"], + }, + }, + args: { + theme: ThemeTypes.Light, + notificationType: "add", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev", + }, + }, +} as Meta; + +const Template = (args: Args) => NotificationFooter({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts new file mode 100644 index 00000000000..49cc1e6bd8d --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts @@ -0,0 +1,39 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { NotificationHeader } from "../../notification/header"; + +type Args = { + message: string; + standalone: boolean; + theme: Theme; + handleCloseNotification: (e: Event) => void; +}; + +export default { + title: "Components/Notifications/Notification Header", + argTypes: { + message: { control: "text" }, + standalone: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + message: "This is a notification message", + standalone: true, + theme: ThemeTypes.Light, + handleCloseNotification: () => alert("Close Clicked"), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-3461&m=dev", + }, + }, +} as Meta; + +const Template = (args: Args) => NotificationHeader({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts new file mode 100644 index 00000000000..4b100764205 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ActionRow } from "../../rows/action-row"; + +type Args = { + itemText: string; + handleAction: (e: Event) => void; + theme: Theme; +}; + +export default { + title: "Components/Rows/Action Row", + argTypes: { + itemText: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + handleAction: { control: false }, + }, + args: { + itemText: "Action Item", + theme: ThemeTypes.Light, + handleAction: () => alert("Action triggered"), + }, +} as Meta; + +const Template = (args: Args) => ActionRow({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts new file mode 100644 index 00000000000..3283c2798a3 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ButtonRow } from "../../rows/button-row"; + +type Args = { + theme: Theme; +}; + +export default { + title: "Components/Rows/Button Row", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + theme: ThemeTypes.Light, + }, +} as Meta; + +const Template = (args: Args) => ButtonRow({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts new file mode 100644 index 00000000000..fbb65201986 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts @@ -0,0 +1,28 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { TemplateResult } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ItemRow } from "../../rows/item-row"; + +type Args = { + theme: Theme; + children: TemplateResult | TemplateResult[]; +}; + +export default { + title: "Components/Rows/Item Row", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + children: { control: "object" }, + }, + args: { + theme: ThemeTypes.Light, + }, +} as Meta; + +const Template = (args: Args) => ItemRow({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 0cce066cf3a..8bd07ab8296 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -91,8 +91,6 @@ function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationTy return i18n.updateLoginPrompt; case NotificationTypes.Unlock: return ""; - case NotificationTypes.FilelessImport: - return ""; default: return undefined; } diff --git a/apps/browser/src/autofill/content/components/package.json b/apps/browser/src/autofill/content/components/package.json new file mode 100644 index 00000000000..8dbe9e7f516 --- /dev/null +++ b/apps/browser/src/autofill/content/components/package.json @@ -0,0 +1,7 @@ +{ + "name": "@bitwarden/lit-components", + "version": "2025.1.1", + "scripts": { + "storybook:lit": "storybook dev -p 6006 -c ./.lit-storybook" + } +} diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts index 226fcb4bd61..a37a2e07678 100644 --- a/apps/browser/src/autofill/content/content-message-handler.spec.ts +++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts @@ -19,6 +19,8 @@ describe("ContentMessageHandler", () => { ); beforeEach(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./content-message-handler"); }); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index b260bfef632..d3e9c29ab58 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ServerConfig } from "../../../../../libs/common/src/platform/abstractions/config/server-config"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; + import { AddLoginMessageData, ChangePasswordMessageData, diff --git a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts index 1ad985bc8e9..317f63e756c 100644 --- a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts +++ b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts @@ -6,6 +6,8 @@ describe("TriggerAutofillScriptInjection", () => { describe("init", () => { it("sends a message to the extension background", () => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("../content/trigger-autofill-script-injection"); expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts index fde98a58a5f..fd6a79733cb 100644 --- a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import AutofillOverlayButton from "./autofill-overlay-button.deprecated"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./legacy-button.scss"); (function () { diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts index 714ccbfbee5..5d587bd4293 100644 --- a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import AutofillOverlayList from "./autofill-overlay-list.deprecated"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./legacy-list.scss"); (function () { diff --git a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts index 1f5abac92b1..5a7b8fa990b 100644 --- a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts +++ b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts @@ -2,7 +2,6 @@ const NotificationQueueMessageType = { AddLogin: "add", ChangePassword: "change", UnlockVault: "unlock", - RequestFilelessImport: "fileless-import", } as const; type NotificationQueueMessageTypes = diff --git a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts index 94bef354a79..8885ed6299c 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts @@ -60,6 +60,8 @@ describe("Fido2 Content Script", () => { chrome.runtime.connect = jest.fn(() => portSpy); it("destroys the messenger when the port is disconnected", () => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); triggerPortOnDisconnectEvent(portSpy); @@ -75,6 +77,8 @@ describe("Fido2 Content Script", () => { const mockResult = { credentialId: "mock" } as CreateCredentialResult; jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); const response = await messenger.handler!(message, new AbortController()); @@ -99,6 +103,8 @@ describe("Fido2 Content Script", () => { data: mock(), }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, new AbortController()); @@ -121,6 +127,8 @@ describe("Fido2 Content Script", () => { const abortController = new AbortController(); const abortSpy = jest.spyOn(abortController.signal, "removeEventListener"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, abortController); @@ -141,6 +149,8 @@ describe("Fido2 Content Script", () => { abortController.abort(); }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, abortController); @@ -161,6 +171,8 @@ describe("Fido2 Content Script", () => { const abortController = new AbortController(); jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); const result = messenger.handler!(message, abortController); @@ -175,6 +187,8 @@ describe("Fido2 Content Script", () => { contentType: "application/json", })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); @@ -193,6 +207,8 @@ describe("Fido2 Content Script", () => { }, })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index 6b9b41b5aac..69e17d26fe5 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -22,6 +22,8 @@ describe("FIDO2 page-script for manifest v2", () => { it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).not.toHaveBeenCalled(); @@ -33,6 +35,8 @@ describe("FIDO2 page-script for manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); @@ -48,6 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts index fd033090cd4..4c1761c37ba 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts @@ -267,6 +267,8 @@ import { Messenger } from "./messaging/messenger"; clearWaitForFocus(); void messenger.destroy(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /** empty */ } diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts index 31e8c941e86..f1aec69193b 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts @@ -55,6 +55,8 @@ describe("Fido2 page script with native WebAuthn support", () => { setupMockedWebAuthnSupport(); beforeAll(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script"); }); @@ -166,6 +168,8 @@ describe("Fido2 page script with native WebAuthn support", () => { contentType: "json/application", })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); @@ -184,6 +188,8 @@ describe("Fido2 page script with native WebAuthn support", () => { }, })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts index e354453ca59..af1838ec942 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts @@ -50,6 +50,8 @@ describe("Fido2 page script without native WebAuthn support", () => { const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); const mockCredentialAssertResult = createAssertCredentialResultMock(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script"); afterEach(() => { diff --git a/apps/browser/src/autofill/models/autofill-field.ts b/apps/browser/src/autofill/models/autofill-field.ts index 7660b4ce5f0..c0be60f1cd0 100644 --- a/apps/browser/src/autofill/models/autofill-field.ts +++ b/apps/browser/src/autofill/models/autofill-field.ts @@ -1,3 +1,4 @@ +import { FieldRect } from "../background/abstractions/overlay.background"; // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { AutofillFieldQualifierType } from "../enums/autofill-field.enums"; @@ -124,4 +125,9 @@ export default class AutofillField { fieldQualifier?: AutofillFieldQualifierType; accountCreationFieldType?: InlineMenuAccountCreationFieldTypes; + + /** + * used for totp multiline calculations + */ + fieldRect?: FieldRect; } diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 2e38adacb32..53948a26a2e 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -4,7 +4,6 @@ const NotificationTypes = { Add: "add", Change: "change", Unlock: "unlock", - FilelessImport: "fileless-import", } as const; type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 6b0e76b5169..b686e1ec2f5 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -55,14 +55,4 @@ - - diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index a4b8ae44b6a..3fc61c448fe 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -4,7 +4,6 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { FilelessImportPort, FilelessImportType } from "../../tools/enums/fileless-import.enums"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -15,6 +14,8 @@ import { NotificationBarIframeInitData, } from "./abstractions/notification-bar"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./bar.scss"); const logService = new ConsoleLogService(false); @@ -57,11 +58,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), - filelessImport: chrome.i18n.getMessage("filelessImport"), - lpFilelessImport: chrome.i18n.getMessage("lpFilelessImport"), - cancelFilelessImport: chrome.i18n.getMessage("no"), - lpCancelFilelessImport: chrome.i18n.getMessage("lpCancelFilelessImport"), - startFilelessImport: chrome.i18n.getMessage("startFilelessImport"), }; setupLogoLink(i18n); @@ -105,22 +101,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc; - // i18n for "Fileless Import" (fileless-import) template - const isLpImport = initData.importType === FilelessImportType.LP; - const importTemplate = document.getElementById("template-fileless-import") as HTMLTemplateElement; - - const startImportButton = importTemplate.content.getElementById("start-fileless-import"); - startImportButton.textContent = i18n.startFilelessImport; - - const cancelImportButton = importTemplate.content.getElementById("cancel-fileless-import"); - cancelImportButton.textContent = isLpImport - ? i18n.lpCancelFilelessImport - : i18n.cancelFilelessImport; - - importTemplate.content.getElementById("fileless-import-text").textContent = isLpImport - ? i18n.lpFilelessImport - : i18n.filelessImport; - // i18n for body content const closeButton = document.getElementById("close-button"); closeButton.title = i18n.close; @@ -132,8 +112,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { handleTypeChange(); } else if (notificationType === "unlock") { handleTypeUnlock(); - } else if (notificationType === "fileless-import") { - handleTypeFilelessImport(); } closeButton.addEventListener("click", (e) => { @@ -247,59 +225,6 @@ function handleTypeUnlock() { }); } -/** - * Sets up a port to communicate with the fileless importer content script. - * This connection to the background script is used to trigger the action of - * downloading the CSV file from the LP importer or importing the data into - * the Bitwarden vault. - */ -function handleTypeFilelessImport() { - const importType = notificationBarIframeInitData.importType; - const port = chrome.runtime.connect({ name: FilelessImportPort.NotificationBar }); - setContent(document.getElementById("template-fileless-import") as HTMLTemplateElement); - - const startFilelessImportButton = document.getElementById("start-fileless-import"); - const startFilelessImport = () => { - port.postMessage({ command: "startFilelessImport", importType }); - document.getElementById("fileless-import-buttons").textContent = - chrome.i18n.getMessage("importing"); - startFilelessImportButton.removeEventListener("click", startFilelessImport); - }; - startFilelessImportButton.addEventListener("click", startFilelessImport); - - const cancelFilelessImportButton = document.getElementById("cancel-fileless-import"); - cancelFilelessImportButton.addEventListener("click", () => { - port.postMessage({ command: "cancelFilelessImport", importType }); - }); - - const handlePortMessage = (msg: any) => { - if (msg.command !== "filelessImportCompleted" && msg.command !== "filelessImportFailed") { - return; - } - - port.disconnect(); - - const filelessImportButtons = document.getElementById("fileless-import-buttons"); - const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper"); - - if (msg.command === "filelessImportCompleted") { - filelessImportButtons.textContent = chrome.i18n.getMessage("dataSuccessfullyImported"); - filelessImportButtons.prepend(buildSvgDomElement(circleCheckIcon)); - filelessImportButtons.classList.add("success-message"); - notificationBarOuterWrapper.classList.add("success-event"); - adjustHeight(); - return; - } - - filelessImportButtons.textContent = chrome.i18n.getMessage("dataImportFailed"); - filelessImportButtons.classList.add("error-message"); - notificationBarOuterWrapper.classList.add("error-event"); - adjustHeight(); - logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`); - }; - port.onMessage.addListener(handlePortMessage); -} - function setContent(template: HTMLTemplateElement) { const content = document.getElementById("content"); while (content.firstChild) { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts index 0ed14a520c1..36ef3897c56 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import { AutofillInlineMenuButton } from "./autofill-inline-menu-button"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./button.scss"); (function () { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts index c302c50b4a4..b46b208b084 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import { AutofillInlineMenuList } from "./autofill-inline-menu-list"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./list.scss"); (function () { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts index 16d5c29d574..522b968e533 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts @@ -1,3 +1,5 @@ +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./menu-container.scss"); import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container"; diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html deleted file mode 100644 index 852fd4a0e81..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
- -
-
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts deleted file mode 100644 index 2101979fe36..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; - -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -@Component({ - selector: "app-fido2-cipher-row-v1", - templateUrl: "fido2-cipher-row-v1.component.html", - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Fido2CipherRowV1Component { - @Output() onSelected = new EventEmitter(); - @Input() cipher: CipherView; - @Input() last: boolean; - @Input() title: string; - @Input() isSearching: boolean; - @Input() isSelected: boolean; - - protected selectCipher(c: CipherView) { - this.onSelected.emit(c); - } - - /** - * Returns a subname for the cipher. - * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. - * @param c Cipher - * @returns - */ - protected getSubName(c: CipherView): string | null { - const fido2Credentials = c.login?.fido2Credentials; - - if (!fido2Credentials || fido2Credentials.length === 0) { - return null; - } - - const [fido2Credential] = fido2Credentials; - - return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; - } -} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html deleted file mode 100644 index 9f6c0aca50d..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - -
- -
-
- -
-
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts deleted file mode 100644 index b7a1bc195ad..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts +++ /dev/null @@ -1,115 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; -import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; - -@Component({ - selector: "app-fido2-use-browser-link-v1", - templateUrl: "fido2-use-browser-link-v1.component.html", - animations: [ - trigger("transformPanel", [ - state( - "void", - style({ - opacity: 0, - }), - ), - transition( - "void => open", - animate( - "100ms linear", - style({ - opacity: 1, - }), - ), - ), - transition("* => void", animate("100ms linear", style({ opacity: 0 }))), - ]), - ], -}) -export class Fido2UseBrowserLinkV1Component { - showOverlay = false; - isOpen = false; - overlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "bottom", - overlayX: "start", - overlayY: "top", - offsetY: 5, - }, - ]; - - protected fido2PopoutSessionData$ = fido2PopoutSessionData$(); - - constructor( - private domainSettingsService: DomainSettingsService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - toggle() { - this.isOpen = !this.isOpen; - } - - close() { - this.isOpen = false; - } - - /** - * Aborts the current FIDO2 session and fallsback to the browser. - * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts. - */ - protected async abort(excludeDomain = true) { - this.close(); - const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); - - if (!excludeDomain) { - this.abortSession(sessionData.sessionId); - return; - } - // Show overlay to prevent the user from interacting with the page. - this.showOverlay = true; - await this.handleDomainExclusion(sessionData.senderUrl); - // Give the user a chance to see the toast before closing the popout. - await Utils.delay(2000); - this.abortSession(sessionData.sessionId); - } - - /** - * Excludes the domain from future FIDO2 prompts. - * @param uri - The domain uri to exclude from future FIDO2 prompts. - */ - private async handleDomainExclusion(uri: string) { - const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); - - const validDomain = Utils.getHostname(uri); - const savedDomains: NeverDomains = { - ...existingDomains, - }; - savedDomains[validDomain] = null; - - await this.domainSettingsService.setNeverDomains(savedDomains); - - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("domainAddedToExcludedDomains", validDomain), - ); - } - - private abortSession(sessionId: string) { - BrowserFido2UserInterfaceSession.abortPopout(sessionId, true); - } -} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html deleted file mode 100644 index 8a052fbc5b7..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html +++ /dev/null @@ -1,142 +0,0 @@ - -
-
-
- - - - - - -
- - -
- -
-
-
- - - -
-

- {{ subtitleText | i18n }} -

- - -
-
- -
-
- -
- -
-
- - -
- -
-
-
-
- -
-

{{ "passkeyAlreadyExists" | i18n }}

-
-
- -
-
- -
-
- -
-

{{ "noPasskeysFoundForThisApplication" | i18n }}

-
- -
-
- - -
-
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts deleted file mode 100644 index 8dc603cfe29..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts +++ /dev/null @@ -1,445 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - concatMap, - filter, - firstValueFrom, - map, - Observable, - Subject, - take, - takeUntil, -} from "rxjs"; - -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; -import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; -import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; -import { - BrowserFido2Message, - BrowserFido2UserInterfaceSession, - BrowserFido2MessageTypes, -} from "../../fido2/services/browser-fido2-user-interface.service"; - -interface ViewData { - message: BrowserFido2Message; - fallbackSupported: boolean; -} - -@Component({ - selector: "app-fido2-v1", - templateUrl: "fido2-v1.component.html", - styleUrls: [], -}) -export class Fido2V1Component implements OnInit, OnDestroy { - private destroy$ = new Subject(); - private hasSearched = false; - - protected cipher: CipherView; - protected searchTypeSearch = false; - protected searchPending = false; - protected searchText: string; - protected url: string; - protected hostname: string; - protected data$: Observable; - protected sessionId?: string; - protected senderTabId?: string; - protected ciphers?: CipherView[] = []; - protected displayedCiphers?: CipherView[] = []; - protected loading = false; - protected subtitleText: string; - protected credentialText: string; - protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; - - private message$ = new BehaviorSubject(null); - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - private cipherService: CipherService, - private platformUtilsService: PlatformUtilsService, - private domainSettingsService: DomainSettingsService, - private searchService: SearchService, - private logService: LogService, - private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, - private passwordRepromptService: PasswordRepromptService, - private fido2UserVerificationService: Fido2UserVerificationService, - private accountService: AccountService, - ) {} - - ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - - const queryParams$ = this.activatedRoute.queryParamMap.pipe( - take(1), - map((queryParamMap) => ({ - sessionId: queryParamMap.get("sessionId"), - senderTabId: queryParamMap.get("senderTabId"), - senderUrl: queryParamMap.get("senderUrl"), - })), - ); - - combineLatest([ - queryParams$, - this.browserMessagingApi.messageListener$() as Observable, - ]) - .pipe( - concatMap(async ([queryParams, message]) => { - this.sessionId = queryParams.sessionId; - this.senderTabId = queryParams.senderTabId; - this.url = queryParams.senderUrl; - // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. - if ( - message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && - message.sessionId !== queryParams.sessionId - ) { - this.abort(false); - return; - } - - // Ignore messages that don't belong to the current session. - if (message.sessionId !== queryParams.sessionId) { - return; - } - - if (message.type === BrowserFido2MessageTypes.AbortRequest) { - this.abort(false); - return; - } - - return message; - }), - filter((message) => !!message), - takeUntil(this.destroy$), - ) - .subscribe((message) => { - this.message$.next(message); - }); - - this.data$ = this.message$.pipe( - filter((message) => message != undefined), - concatMap(async (message) => { - switch (message.type) { - case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); - - this.ciphers = (await this.cipherService.getAllDecrypted()).filter( - (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, - ); - this.displayedCiphers = this.ciphers.filter( - (cipher) => - cipher.login.matchesUri(this.url, equivalentDomains) && - this.hasNoOtherPasskeys(cipher, message.userHandle), - ); - - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - - case BrowserFido2MessageTypes.PickCredentialRequest: { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.ciphers = await Promise.all( - message.cipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - }), - ); - this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - - case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.ciphers = await Promise.all( - message.existingCipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - }), - ); - this.displayedCiphers = [...this.ciphers]; - - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - } - - this.subtitleText = - this.displayedCiphers.length > 0 - ? this.getCredentialSubTitleText(message.type) - : "noMatchingPasskeyLogin"; - - this.credentialText = this.getCredentialButtonText(message.type); - return { - message, - fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, - }; - }), - takeUntil(this.destroy$), - ); - - queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { - this.send({ - sessionId: queryParams.sessionId, - type: BrowserFido2MessageTypes.ConnectResponse, - }); - }); - } - - protected async submit() { - const data = this.message$.value; - if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); - - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher.id, - type: BrowserFido2MessageTypes.PickCredentialResponse, - userVerified, - }); - } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - if (this.cipher.login.hasFido2Credentials) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "overwritePasskey" }, - content: { key: "overwritePasskeyAlert" }, - type: "info", - }); - - if (!confirmed) { - return false; - } - } - - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); - - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher.id, - type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, - userVerified, - }); - } - - this.loading = true; - } - - protected async saveNewLogin() { - const data = this.message$.value; - if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - const name = data.credentialName || data.rpId; - // TODO: Revert to check for user verification once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - await this.createNewCipher(name, data.userName); - - // We are bypassing user verification pending approval. - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher?.id, - type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, - userVerified: data.userVerification, - }); - } - - this.loading = true; - } - - getCredentialSubTitleText(messageType: string): string { - return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest - ? "chooseCipherForPasskeySave" - : "logInWithPasskeyQuestion"; - } - - getCredentialButtonText(messageType: string): string { - return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest - ? "savePasskey" - : "confirm"; - } - - selectedPasskey(item: CipherView) { - this.cipher = item; - } - - viewPasskey() { - // 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.router.navigate(["/view-cipher"], { - queryParams: { - cipherId: this.cipher.id, - uilocation: "popout", - senderTabId: this.senderTabId, - sessionId: this.sessionId, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - addCipher() { - const data = this.message$.value; - - if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - return; - } - - // 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.router.navigate(["/add-cipher"], { - queryParams: { - name: data.credentialName || data.rpId, - uri: this.url, - type: CipherType.Login.toString(), - uilocation: "popout", - username: data.userName, - senderTabId: this.senderTabId, - sessionId: this.sessionId, - userVerification: data.userVerification, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - protected async search() { - this.hasSearched = await this.searchService.isSearchable(this.searchText); - this.searchPending = true; - if (this.hasSearched) { - this.displayedCiphers = await this.searchService.searchCiphers( - this.searchText, - null, - this.ciphers, - ); - } else { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); - this.displayedCiphers = this.ciphers.filter((cipher) => - cipher.login.matchesUri(this.url, equivalentDomains), - ); - } - this.searchPending = false; - this.selectedPasskey(this.displayedCiphers[0]); - } - - abort(fallback: boolean) { - this.unload(fallback); - window.close(); - } - - unload(fallback = false) { - this.send({ - sessionId: this.sessionId, - type: BrowserFido2MessageTypes.AbortResponse, - fallbackRequested: fallback, - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private buildCipher(name: string, username: string) { - this.cipher = new CipherView(); - this.cipher.name = name; - - this.cipher.type = CipherType.Login; - this.cipher.login = new LoginView(); - this.cipher.login.username = username; - this.cipher.login.uris = [new LoginUriView()]; - this.cipher.login.uris[0].uri = this.url; - this.cipher.card = new CardView(); - this.cipher.identity = new IdentityView(); - this.cipher.secureNote = new SecureNoteView(); - this.cipher.secureNote.type = SecureNoteType.Generic; - this.cipher.reprompt = CipherRepromptType.None; - } - - private async createNewCipher(name: string, username: string) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.buildCipher(name, username); - const cipher = await this.cipherService.encrypt(this.cipher, activeUserId); - try { - await this.cipherService.createWithServer(cipher); - this.cipher.id = cipher.id; - } catch (e) { - this.logService.error(e); - } - } - - // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. - private async handleUserVerification( - userVerificationRequested: boolean, - cipher: CipherView, - ): Promise { - const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0; - - if (masterPasswordRepromptRequired) { - return await this.passwordRepromptService.showPasswordPrompt(); - } - - return userVerificationRequested; - } - - private send(msg: BrowserFido2Message) { - BrowserFido2UserInterfaceSession.sendMessage({ - sessionId: this.sessionId, - ...msg, - }); - } - - /** - * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle - * @param userHandle - */ - private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { - if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { - return true; - } - - return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); - } -} diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html deleted file mode 100644 index 1c16ee1fe12..00000000000 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html +++ /dev/null @@ -1,270 +0,0 @@ -
-
- -
-

- {{ "autofill" | i18n }} -

-
-
-
-
-
- -
- -
-
-
-
- - -
- -
-
-
-
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-

{{ "additionalOptions" | i18n }}

-
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
-
- -
-
-
diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts deleted file mode 100644 index 9f015d990e9..00000000000 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts +++ /dev/null @@ -1,344 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { - InlineMenuVisibilitySetting, - ClearClipboardDelaySetting, -} from "@bitwarden/common/autofill/types"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { - UriMatchStrategy, - UriMatchStrategySetting, -} from "@bitwarden/common/models/domain/domain-service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { DialogService } from "@bitwarden/components"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "app-autofill-v1", - templateUrl: "autofill-v1.component.html", -}) -export class AutofillV1Component implements OnInit { - protected canOverrideBrowserAutofillSetting = false; - protected defaultBrowserAutofillDisabled = false; - protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; - protected autoFillOverlayVisibilityOptions: any[]; - protected disablePasswordManagerLink: string; - protected inlineMenuPositioningImprovementsEnabled: boolean = false; - protected blockBrowserInjectionsByDomainEnabled: boolean = false; - protected showInlineMenuIdentities: boolean = true; - protected showInlineMenuCards: boolean = true; - inlineMenuIsEnabled: boolean = false; - enableAutoFillOnPageLoad = false; - autoFillOnPageLoadDefault = false; - autoFillOnPageLoadOptions: any[]; - enableContextMenuItem = false; - enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true? - clearClipboard: ClearClipboardDelaySetting; - clearClipboardOptions: any[]; - defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; - uriMatchOptions: any[]; - showCardsCurrentTab = false; - showIdentitiesCurrentTab = false; - autofillKeyboardHelperText: string; - accountSwitcherEnabled = false; - - constructor( - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private domainSettingsService: DomainSettingsService, - private configService: ConfigService, - private dialogService: DialogService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private messagingService: MessagingService, - private vaultSettingsService: VaultSettingsService, - ) { - this.autoFillOverlayVisibilityOptions = [ - { - name: i18nService.t("autofillOverlayVisibilityOff"), - value: AutofillOverlayVisibility.Off, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnFieldFocus"), - value: AutofillOverlayVisibility.OnFieldFocus, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnButtonClick"), - value: AutofillOverlayVisibility.OnButtonClick, - }, - ]; - this.autoFillOnPageLoadOptions = [ - { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, - { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, - ]; - this.clearClipboardOptions = [ - { name: i18nService.t("never"), value: null }, - { name: i18nService.t("tenSeconds"), value: 10 }, - { name: i18nService.t("twentySeconds"), value: 20 }, - { name: i18nService.t("thirtySeconds"), value: 30 }, - { name: i18nService.t("oneMinute"), value: 60 }, - { name: i18nService.t("twoMinutes"), value: 120 }, - { name: i18nService.t("fiveMinutes"), value: 300 }, - ]; - this.uriMatchOptions = [ - { name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain }, - { name: i18nService.t("host"), value: UriMatchStrategy.Host }, - { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, - { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, - { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, - { name: i18nService.t("never"), value: UriMatchStrategy.Never }, - ]; - - this.accountSwitcherEnabled = enableAccountSwitching(); - this.disablePasswordManagerLink = this.getDisablePasswordManagerLink(); - } - - async ngOnInit() { - this.canOverrideBrowserAutofillSetting = - this.platformUtilsService.isChrome() || - this.platformUtilsService.isEdge() || - this.platformUtilsService.isOpera() || - this.platformUtilsService.isVivaldi(); - - this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden(); - - this.autoFillOverlayVisibility = await firstValueFrom( - this.autofillSettingsService.inlineMenuVisibility$, - ); - - this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( - FeatureFlag.InlineMenuPositioningImprovements, - ); - - this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag( - FeatureFlag.BlockBrowserInjectionsByDomain, - ); - - this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); - - this.showInlineMenuIdentities = - this.inlineMenuPositioningImprovementsEnabled && - (await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$)); - - this.showInlineMenuCards = - this.inlineMenuPositioningImprovementsEnabled && - (await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$)); - - this.enableAutoFillOnPageLoad = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoad$, - ); - - this.autoFillOnPageLoadDefault = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoadDefault$, - ); - - this.enableContextMenuItem = await firstValueFrom( - this.autofillSettingsService.enableContextMenu$, - ); - - this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); - - this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); - - const defaultUriMatch = await firstValueFrom( - this.domainSettingsService.defaultUriMatchStrategy$, - ); - this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; - - const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); - await this.setAutofillKeyboardHelperText(command); - - this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$); - - this.showIdentitiesCurrentTab = await firstValueFrom( - this.vaultSettingsService.showIdentitiesCurrentTab$, - ); - } - - isInlineMenuEnabled() { - return ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus || - this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick - ); - } - - async updateAutoFillOverlayVisibility() { - await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); - await this.requestPrivacyPermission(); - - this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); - } - - async updateAutoFillOnPageLoad() { - await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); - } - - async updateAutoFillOnPageLoadDefault() { - await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); - } - - async saveDefaultUriMatch() { - await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch); - } - - private async setAutofillKeyboardHelperText(command: string) { - if (command) { - this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command); - } else { - this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet"); - } - } - - async commandSettings() { - if (this.platformUtilsService.isChrome()) { - // 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("chrome://extensions/shortcuts"); - } else if (this.platformUtilsService.isOpera()) { - // 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("opera://extensions/shortcuts"); - } else if (this.platformUtilsService.isEdge()) { - // 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("edge://extensions/shortcuts"); - } else if (this.platformUtilsService.isVivaldi()) { - // 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("vivaldi://extensions/shortcuts"); - } else { - // 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/keyboard-shortcuts"); - } - } - - private getDisablePasswordManagerLink(): string { - if (this.platformUtilsService.isChrome()) { - return "chrome://settings/autofill"; - } - if (this.platformUtilsService.isOpera()) { - return "opera://settings/autofill"; - } - if (this.platformUtilsService.isEdge()) { - return "edge://settings/passwords"; - } - if (this.platformUtilsService.isVivaldi()) { - return "vivaldi://settings/autofill"; - } - - return "https://bitwarden.com/help/disable-browser-autofill/"; - } - - protected openDisablePasswordManagerLink(event: Event) { - event.preventDefault(); - // 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(this.disablePasswordManagerLink); - } - - async requestPrivacyPermission() { - if ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off || - !this.canOverrideBrowserAutofillSetting || - (await this.browserAutofillSettingCurrentlyOverridden()) - ) { - return; - } - - await this.dialogService.openSimpleDialog({ - title: { key: "overrideDefaultBrowserAutofillTitle" }, - content: { key: "overrideDefaultBrowserAutofillDescription" }, - acceptButtonText: { key: "makeDefault" }, - acceptAction: async () => await this.handleOverrideDialogAccept(), - cancelButtonText: { key: "ignore" }, - type: "info", - }); - } - - async updateDefaultBrowserAutofillDisabled() { - const privacyPermissionGranted = await this.privacyPermissionGranted(); - if (!this.defaultBrowserAutofillDisabled && !privacyPermissionGranted) { - return; - } - - if ( - !privacyPermissionGranted && - !(await BrowserApi.requestPermission({ permissions: ["privacy"] })) - ) { - await this.dialogService.openSimpleDialog({ - title: { key: "privacyPermissionAdditionNotGrantedTitle" }, - content: { key: "privacyPermissionAdditionNotGrantedDescription" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "warning", - }); - this.defaultBrowserAutofillDisabled = false; - - return; - } - - BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled); - } - - private handleOverrideDialogAccept = async () => { - this.defaultBrowserAutofillDisabled = true; - await this.updateDefaultBrowserAutofillDisabled(); - }; - - async browserAutofillSettingCurrentlyOverridden() { - if (!this.canOverrideBrowserAutofillSetting) { - return false; - } - - if (!(await this.privacyPermissionGranted())) { - return false; - } - - return await BrowserApi.browserAutofillSettingsOverridden(); - } - - async privacyPermissionGranted(): Promise { - return await BrowserApi.permissionsGranted(["privacy"]); - } - - async updateContextMenuItem() { - await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); - this.messagingService.send("bgUpdateContextMenu"); - } - - async updateAutoTotpCopy() { - await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy); - } - - async saveClearClipboard() { - await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); - } - - async updateShowCardsCurrentTab() { - await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); - } - - async updateShowIdentitiesCurrentTab() { - await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); - } - - async updateShowInlineMenuCards() { - await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards); - } - - async updateShowInlineMenuIdentities() { - await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities); - } -} diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index eeae0a85e3f..e8299f01166 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -118,7 +118,7 @@

{{ "autofillSuggestionsSectionTitle" | i18n }}

(change)="updateShowCardsCurrentTab()" [(ngModel)]="showCardsCurrentTab" /> - {{ "showCardsInVaultView" | i18n }} + {{ "showCardsInVaultViewV2" | i18n }}
{{ "autofillSuggestionsSectionTitle" | i18n }} [(ngModel)]="showIdentitiesCurrentTab" /> - {{ "showIdentitiesInVaultView" | i18n }} + {{ "showIdentitiesInVaultViewV2" | i18n }} diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html deleted file mode 100644 index 8f78ac16404..00000000000 --- a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html +++ /dev/null @@ -1,91 +0,0 @@ -
-
-
- -
-

- {{ "excludedDomains" | i18n }} -

-
- -
-
-
-
-
- - -
- -
- - - - -
-
- -
-
-
- -
-
-
-
diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts deleted file mode 100644 index 1a7dcd9c2dc..00000000000 --- a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts +++ /dev/null @@ -1,143 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; - -interface ExcludedDomain { - uri: string; - showCurrentUris: boolean; -} - -const BroadcasterSubscriptionId = "excludedDomains"; - -@Component({ - selector: "app-excluded-domains-v1", - templateUrl: "excluded-domains-v1.component.html", -}) -export class ExcludedDomainsV1Component implements OnInit, OnDestroy { - excludedDomains: ExcludedDomain[] = []; - existingExcludedDomains: ExcludedDomain[] = []; - currentUris: string[]; - loadCurrentUrisTimeout: number; - accountSwitcherEnabled = false; - - constructor( - private domainSettingsService: DomainSettingsService, - private i18nService: I18nService, - private router: Router, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); - if (savedDomains) { - for (const uri of Object.keys(savedDomains)) { - this.excludedDomains.push({ uri: uri, showCurrentUris: false }); - this.existingExcludedDomains.push({ uri: uri, showCurrentUris: false }); - } - } - - await this.loadCurrentUris(); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // 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.ngZone.run(async () => { - switch (message.command) { - case "tabChanged": - case "windowChanged": - if (this.loadCurrentUrisTimeout != null) { - window.clearTimeout(this.loadCurrentUrisTimeout); - } - this.loadCurrentUrisTimeout = window.setTimeout( - async () => await this.loadCurrentUris(), - 500, - ); - break; - default: - break; - } - }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async addUri() { - this.excludedDomains.push({ uri: "", showCurrentUris: false }); - } - - async removeUri(i: number) { - this.excludedDomains.splice(i, 1); - } - - async submit() { - const savedDomains: { [name: string]: null } = {}; - const newExcludedDomains = this.getNewlyAddedDomains(this.excludedDomains); - for (const domain of this.excludedDomains) { - const resp = newExcludedDomains.filter((e) => e.uri === domain.uri); - if (resp.length === 0) { - savedDomains[domain.uri] = null; - } else { - if (domain.uri && domain.uri !== "") { - const validDomain = Utils.getHostname(domain.uri); - if (!validDomain) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("excludedDomainsInvalidDomain", domain.uri), - ); - return; - } - savedDomains[validDomain] = null; - } - } - } - - await this.domainSettingsService.setNeverDomains(savedDomains); - // 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.router.navigate(["/tabs/settings"]); - } - - trackByFunction(index: number, item: any) { - return index; - } - - getNewlyAddedDomains(domain: ExcludedDomain[]): ExcludedDomain[] { - const result = this.excludedDomains.filter( - (newDomain) => - !this.existingExcludedDomains.some((oldDomain) => newDomain.uri === oldDomain.uri), - ); - return result; - } - - toggleUriInput(domain: ExcludedDomain) { - domain.showCurrentUris = !domain.showCurrentUris; - } - - async loadCurrentUris() { - const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); - if (tabs) { - const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url))); - uriSet.delete(null); - this.currentUris = Array.from(uriSet); - } - } -} diff --git a/apps/browser/src/autofill/popup/settings/notifications-v1.component.html b/apps/browser/src/autofill/popup/settings/notifications-v1.component.html deleted file mode 100644 index 89d83c9e480..00000000000 --- a/apps/browser/src/autofill/popup/settings/notifications-v1.component.html +++ /dev/null @@ -1,89 +0,0 @@ -
-
- -
-

- {{ "notifications" | i18n }} -

-
- -
-
-
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
- -
-
-
diff --git a/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts b/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts deleted file mode 100644 index 442e91e55eb..00000000000 --- a/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; - -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "autofill-notification-v1-settings", - templateUrl: "notifications-v1.component.html", -}) -export class NotificationsSettingsV1Component implements OnInit { - enableAddLoginNotification = false; - enableChangedPasswordNotification = false; - enablePasskeys = true; - accountSwitcherEnabled = false; - - constructor( - private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, - private vaultSettingsService: VaultSettingsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - this.enableAddLoginNotification = await firstValueFrom( - this.userNotificationSettingsService.enableAddedLoginPrompt$, - ); - - this.enableChangedPasswordNotification = await firstValueFrom( - this.userNotificationSettingsService.enableChangedPasswordPrompt$, - ); - - this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); - } - - async updateAddLoginNotification() { - await this.userNotificationSettingsService.setEnableAddedLoginPrompt( - this.enableAddLoginNotification, - ); - } - - async updateChangedPasswordNotification() { - await this.userNotificationSettingsService.setEnableChangedPasswordPrompt( - this.enableChangedPasswordNotification, - ); - } - - async updateEnablePasskeys() { - await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys); - } -} diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index daa65d74ae6..acf0dedde27 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -957,8 +957,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ accountCreationFieldType: autofillFieldData?.accountCreationFieldType, }; + const allFields = this.formFieldElements; + const allFieldsRect = []; + + for (const key of allFields.keys()) { + const rect = await this.getMostRecentlyFocusedFieldRects(key); + allFieldsRect.push({ ...allFields.get(key), rect }); // Add the combined result to the array + } + await this.sendExtensionMessage("updateFocusedFieldData", { focusedFieldData: this.focusedFieldData, + allFieldsRect, }); } @@ -1408,6 +1417,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ url.origin + pathWithoutTrailingSlashAndSearch, url.origin + pathWithoutTrailingSlashSearchAndHash, ]); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_error) { return null; } diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 13c546bccdb..b858af25fae 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -982,7 +982,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ const queueLength = this.mutationsQueue.length; if (!this.domQueryService.pageContainsShadowDomElements()) { - this.checkPageContainsShadowDom(); + // Checking if a page contains shadowDOM elements is a heavy operation and doesn't have to be done immediately, so we can call this within an idle moment on the event loop. + requestIdleCallbackPolyfill(this.checkPageContainsShadowDom, { timeout: 500 }); } for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) { diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts index 0dbc246b235..16310397a03 100644 --- a/apps/browser/src/autofill/services/dom-query.service.ts +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -235,6 +235,8 @@ export class DomQueryService implements DomQueryServiceInterface { if ((chrome as any).dom?.openOrClosedShadowRoot) { try { return (chrome as any).dom.openOrClosedShadowRoot(node); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return null; } diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index a14ef1330cc..58ac95e7edf 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,5 +1,5 @@ +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; -import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; import { CipherType } from "@bitwarden/common/vault/enums"; diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 12d26914d82..614a5b014f2 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { FieldRect } from "../background/abstractions/overlay.background"; import { AutofillPort } from "../enums/autofill-port.enum"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; @@ -36,7 +37,9 @@ export function requestIdleCallbackPolyfill( return globalThis.requestIdleCallback(() => callback(), options); } - return globalThis.setTimeout(() => callback(), 1); + const timeoutDelay = options?.timeout || 1; + + return globalThis.setTimeout(() => callback(), timeoutDelay); } /** @@ -545,6 +548,17 @@ export const specialCharacterToKeyMap: Record = { "/": "forwardSlashCharacterDescriptor", }; +/** + * Determines if the current rect values are not all 0. + */ +export function rectHasSize(rect: FieldRect): boolean { + if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) { + return true; + } + + return false; +} + /** * Checks if all the values corresponding to the specified keys in an object are null. * If no keys are specified, checks all keys in the object. diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4bec3d6cc0a..192455f9691 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -25,7 +25,7 @@ import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin- import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; @@ -92,6 +92,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -125,6 +126,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StateService } from "@bitwarden/common/platform/services/state.service"; @@ -260,7 +262,7 @@ import { LocalBackedSessionStorageService } from "../platform/services/local-bac import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; -import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../platform/services/sdk/browser-sdk-load.service"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; @@ -268,7 +270,6 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; -import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -380,6 +381,7 @@ export default class MainBackground { themeStateService: DefaultThemeStateService; autoSubmitLoginBackground: AutoSubmitLoginBackground; sdkService: SdkService; + sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; @@ -393,7 +395,6 @@ export default class MainBackground { private notificationBackground: NotificationBackground; private overlayBackground: OverlayBackgroundInterface; private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface; - private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; @@ -632,11 +633,6 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); - this.biometricsService = new BackgroundBrowserBiometricsService( - runtimeNativeMessagingBackground, - this.logService, - ); - this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); this.pinService = new PinService( @@ -665,10 +661,18 @@ export default class MainBackground { this.kdfConfigService, ); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + this.logService, + this.keyService, + this.biometricStateService, + this.messagingService, + ); + this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); + this.organizationService = new DefaultOrganizationService(this.stateProvider); this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( @@ -729,8 +733,9 @@ export default class MainBackground { ); const sdkClientFactory = flagEnabled("sdk") - ? new BrowserSdkClientFactory(this.logService) + ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); + this.sdkLoadService = new BrowserSdkLoadService(this.logService); this.sdkService = new DefaultSdkService( sdkClientFactory, this.environmentService, @@ -1157,16 +1162,6 @@ export default class MainBackground { this.notificationBackground, ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - this.scriptInjectorService, - ); - this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( this.logService, this.autofillService, @@ -1257,6 +1252,7 @@ export default class MainBackground { this.cipherAuthorizationService = new DefaultCipherAuthorizationService( this.collectionService, this.organizationService, + this.accountService, ); this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); @@ -1265,6 +1261,7 @@ export default class MainBackground { async bootstrap() { this.containerService.attachToGlobal(self); + await this.sdkLoadService.load(); // Only the "true" background should run migrations await this.stateService.init({ runMigrations: true }); @@ -1293,7 +1290,6 @@ export default class MainBackground { await this.runtimeBackground.init(); await this.notificationBackground.init(); this.overlayNotificationsBackground.init(); - this.filelessImporterBackground.init(); this.commandsBackground.init(); this.contextMenusBackground?.init(); this.idleBackground.init(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 116d048d2e8..21c8f351e8d 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -162,10 +162,6 @@ export class NativeMessagingBackground { HashAlgorithmForEncryption, ); - if (this.validatingFingerprint) { - this.validatingFingerprint = false; - await this.biometricStateService.setFingerprintValidated(true); - } this.sharedSecret = new SymmetricCryptoKey(decrypted); this.logService.info("[Native Messaging IPC] Secure channel established"); @@ -200,15 +196,24 @@ export class NativeMessagingBackground { } return; case "verifyFingerprint": { - if (this.sharedSecret == null) { - this.logService.info( - "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.", - ); - this.validatingFingerprint = true; - // 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.showFingerprintDialog(); - } + this.logService.info("[Native Messaging IPC] Legacy app is requesting fingerprint"); + this.messagingService.send("showUpdateDesktopAppOrDisableFingerprintDialog", {}); + break; + } + case "verifyDesktopIPCFingerprint": { + this.logService.info( + "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.", + ); + await this.showFingerprintDialog(); + break; + } + case "verifiedDesktopIPCFingerprint": { + await this.biometricStateService.setFingerprintValidated(true); + this.messagingService.send("hideNativeMessagingFingerprintDialog", {}); + break; + } + case "rejectedDesktopIPCFingerprint": { + this.messagingService.send("hideNativeMessagingFingerprintDialog", {}); break; } case "wrongUserId": @@ -257,7 +262,7 @@ export class NativeMessagingBackground { message.command == BiometricsCommands.Unlock || message.command == BiometricsCommands.IsAvailable ) { - // TODO remove after 2025.01 + // TODO remove after 2025.3 // wait until there is no other callbacks, or timeout const call = await firstValueFrom( race( @@ -342,6 +347,8 @@ export class NativeMessagingBackground { }; } this.port.postMessage(msg); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.logService.info( "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.", @@ -426,12 +433,9 @@ export class NativeMessagingBackground { } private async showFingerprintDialog() { - const fingerprint = await this.keyService.getFingerprint( - (await firstValueFrom(this.accountService.activeAccount$))?.id, - this.publicKey, - ); + const fingerprint = await this.keyService.getFingerprint(this.appId, this.publicKey); - this.messagingService.send("showNativeMessagingFinterprintDialog", { + this.messagingService.send("showNativeMessagingFingerprintDialog", { fingerprint: fingerprint, }); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 863ca26b36e..2f038946bf9 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -22,7 +22,7 @@ import { BiometricsCommands } from "@bitwarden/key-management"; import { closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, } from "../auth/popup/utils/auth-popout-window"; import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background"; import { AutofillService } from "../autofill/services/abstractions/autofill.service"; @@ -259,9 +259,7 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(false); - if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { - await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); - } + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); break; } case "addToLockedVaultPendingNotifications": @@ -288,9 +286,7 @@ export default class RuntimeBackground { await this.configService.ensureConfigFetched(); await this.main.updateOverlayCiphers(); - if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { - await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); - } + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); } break; case "openPopup": @@ -337,7 +333,7 @@ export default class RuntimeBackground { return; } - await openTwoFactorAuthPopout(msg); + await openTwoFactorAuthWebAuthnPopout(msg); break; } case "reloadPopup": diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts index f658f71a209..da3d2c07e4b 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -20,6 +20,7 @@ import { DialogService, ItemModule, SectionComponent, + ToastService, } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -57,6 +58,7 @@ export class PremiumV2Component extends BasePremiumComponent { dialogService: DialogService, environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + toastService: ToastService, accountService: AccountService, ) { super( @@ -68,6 +70,7 @@ export class PremiumV2Component extends BasePremiumComponent { dialogService, environmentService, billingAccountProfileStateService, + toastService, accountService, ); diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts index 8e6fc562d14..3031134dc34 100644 --- a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts +++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts @@ -1,9 +1,18 @@ import { Injectable } from "@angular/core"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; -import { BiometricsService, BiometricsCommands, BiometricsStatus } from "@bitwarden/key-management"; +import { + BiometricsService, + BiometricsCommands, + BiometricsStatus, + KeyService, + BiometricStateService, +} from "@bitwarden/key-management"; import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -13,6 +22,9 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { constructor( private nativeMessagingBackground: () => NativeMessagingBackground, private logService: LogService, + private keyService: KeyService, + private biometricStateService: BiometricStateService, + private messagingService: MessagingService, ) { super(); } @@ -65,6 +77,8 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { } } return BiometricsStatus.Available; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return BiometricsStatus.DesktopDisconnected; } @@ -74,12 +88,21 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { try { await this.ensureConnected(); + // todo remove after 2025.3 if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { const response = await this.nativeMessagingBackground().callCommand({ command: BiometricsCommands.Unlock, }); if (response.response == "unlocked") { - return response.userKeyB64; + const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64); + const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey; + if (await this.keyService.validateUserKey(userKey, userId)) { + await this.biometricStateService.setBiometricUnlockEnabled(true); + await this.keyService.setUserKey(userKey, userId); + // to update badge and other things + this.messagingService.send("switchAccount", { userId }); + return userKey; + } } else { return null; } @@ -89,7 +112,16 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { userId: userId, }); if (response.response) { - return response.userKeyB64; + // In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled + const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64); + const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey; + if (await this.keyService.validateUserKey(userKey, userId)) { + await this.biometricStateService.setBiometricUnlockEnabled(true); + await this.keyService.setUserKey(userKey, userId); + // to update badge and other things + this.messagingService.send("switchAccount", { userId }); + return userKey; + } } else { return null; } @@ -98,6 +130,8 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { this.logService.info("Biometric unlock for user failed", e); throw new Error("Biometric unlock failed"); } + + return null; } async getBiometricsStatusForUser(id: UserId): Promise { @@ -114,6 +148,8 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { userId: id, }) ).response; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return BiometricsStatus.DesktopDisconnected; } diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts index 0235ad5bd9c..d248a630cc6 100644 --- a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts +++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts @@ -29,13 +29,13 @@ export class ForegroundBrowserBiometricsService extends BiometricsService { async unlockWithBiometricsForUser(userId: UserId): Promise { const response = await BrowserApi.sendMessageWithResponse<{ - result: string; + result: UserKey; error: string; }>(BiometricsCommands.UnlockWithBiometricsForUser, { userId }); if (!response.result) { return null; } - return SymmetricCryptoKey.fromString(response.result) as UserKey; + return SymmetricCryptoKey.fromString(response.result.keyB64) as UserKey; } async getBiometricsStatusForUser(id: UserId): Promise { diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts index 4b0323d5ebe..92830c35aca 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts @@ -9,7 +9,12 @@ import { import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { KeyService, BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; +import { + KeyService, + BiometricsService, + BiometricsStatus, + BiometricStateService, +} from "@bitwarden/key-management"; import { UnlockOptions } from "@bitwarden/key-management/angular"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; @@ -26,6 +31,7 @@ describe("ExtensionLockComponentService", () => { let vaultTimeoutSettingsService: MockProxy; let keyService: MockProxy; let routerService: MockProxy; + let biometricStateService: MockProxy; beforeEach(() => { userDecryptionOptionsService = mock(); @@ -35,6 +41,7 @@ describe("ExtensionLockComponentService", () => { vaultTimeoutSettingsService = mock(); keyService = mock(); routerService = mock(); + biometricStateService = mock(); TestBed.configureTestingModule({ providers: [ @@ -67,6 +74,10 @@ describe("ExtensionLockComponentService", () => { provide: BrowserRouterService, useValue: routerService, }, + { + provide: BiometricStateService, + useValue: biometricStateService, + }, ], }); @@ -306,6 +317,7 @@ describe("ExtensionLockComponentService", () => { platformUtilsService.supportsSecureStorage.mockReturnValue( mockInputs.platformSupportsSecureStorage, ); + biometricStateService.biometricUnlockEnabled$ = of(true); // PIN pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts index f21beb91cff..711ae4e378f 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts @@ -1,14 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { inject } from "@angular/core"; -import { combineLatest, defer, map, Observable } from "rxjs"; +import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs"; import { PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; -import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; +import { + BiometricsService, + BiometricsStatus, + BiometricStateService, +} from "@bitwarden/key-management"; import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; @@ -19,6 +23,7 @@ export class ExtensionLockComponentService implements LockComponentService { private readonly biometricsService = inject(BiometricsService); private readonly pinService = inject(PinServiceAbstraction); private readonly routerService = inject(BrowserRouterService); + private readonly biometricStateService = inject(BiometricStateService); getPreviousUrl(): string | null { return this.routerService.getPreviousUrl(); @@ -45,7 +50,21 @@ export class ExtensionLockComponentService implements LockComponentService { getAvailableUnlockOptions$(userId: UserId): Observable { return combineLatest([ // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to - defer(async () => await this.biometricsService.getBiometricsStatusForUser(userId)), + defer(async () => { + if (!(await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$))) { + return BiometricsStatus.NotEnabledLocally; + } else { + // TODO remove after 2025.3 + // remove after backward compatibility code for old biometrics ipc protocol is removed + const result: BiometricsStatus = (await Promise.race([ + this.biometricsService.getBiometricsStatusForUser(userId), + new Promise((resolve) => + setTimeout(() => resolve(BiometricsStatus.DesktopDisconnected), 1000), + ), + ])) as BiometricsStatus; + return result; + } + }), this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), defer(() => this.pinService.isPinDecryptionAvailable(userId)), ]).pipe( diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 9e4eb78291a..90ae271fd14 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.1.0", + "version": "2025.1.4", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -29,12 +29,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { @@ -140,7 +134,6 @@ }, "web_accessible_resources": [ "content/fido2-page-script.js", - "content/lp-suppress-import-download.js", "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index ef7ede6b056..988b6eeb9c5 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.1.0", + "version": "2025.1.4", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -30,12 +30,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 383e982f065..2b1040bcd8a 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -11,13 +11,11 @@ import { GroupPolicyEnvironment } from "../admin-console/types/group-policy-envi import { BrowserApi } from "./browser/browser-api"; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = { accountSwitching?: boolean; } & SharedFlags; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { managedEnvironment?: GroupPolicyEnvironment; } & SharedDevFlags; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 4065f2e46d7..67fa920d18d 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -8,6 +8,8 @@ describe("OffscreenDocument", () => { const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); const consoleErrorSpy = jest.spyOn(console, "error"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("../offscreen-document/offscreen-document"); describe("init", () => { diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index c1ac8823261..43693ca223b 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -45,7 +45,7 @@ class ExtensionContainerComponent {} - - + + @@ -306,6 +306,10 @@ export default { // Disable tests while we troubleshoot their flaky-ness disableSnapshot: true, }, + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-38889&t=k6OTDDPZOTtypRqo-11", + }, }, decorators: [ moduleMetadata({ diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index a717786ae52..69a0fa7d9d1 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -133,6 +133,7 @@ export class PopupViewCacheService implements ViewCacheService { } private clearState() { + this._cache = {}; // clear local cache this.messageSender.send(ClEAR_VIEW_CACHE_COMMAND, {}); } } diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts index fbe94bece8c..72cfd39cd62 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts @@ -196,13 +196,15 @@ describe("popup view cache", () => { }); it("should clear on 2nd navigation", async () => { - await initServiceWithState({}); + await initServiceWithState({ temp: "state" }); await router.navigate(["a"]); expect(messageSenderMock.send).toHaveBeenCalledTimes(0); + expect(service["_cache"]).toEqual({ temp: "state" }); await router.navigate(["b"]); expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, {}); + expect(service["_cache"]).toEqual({}); }); it("should ignore cached values when feature flag is off", async () => { diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts index 0919de46776..b177497305b 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -63,7 +63,7 @@ describe("ScriptInjectorService", () => { configService.getFeatureFlag$.mockImplementation(() => of(false)); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); - domainSettingsService.blockedInteractionsUris$ = of(null); + domainSettingsService.blockedInteractionsUris$ = of({}); scriptInjectorService = new BrowserScriptInjectorService( domainSettingsService, platformUtilsService, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 949ecebde8a..4d6a403a18a 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -48,6 +48,8 @@ describe("LocalBackedSessionStorage", () => { localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); const result = await sut.get("test"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( encrypted, sessionKey, @@ -69,6 +71,8 @@ describe("LocalBackedSessionStorage", () => { localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); const result = await sut.get("test"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( encrypted, sessionKey, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index bdc0bed80c0..900212ddefa 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -198,6 +198,8 @@ export class LocalBackedSessionStorageService private compareValues(value1: T, value2: T): boolean { try { return compareValues(value1, value2); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.logService.error( `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`, diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts index f6f5e45f093..e739a3677f1 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -60,6 +60,11 @@ export class PopupViewCacheBackgroundService { ) .subscribe(); + this.messageListener + .messages$(ClEAR_VIEW_CACHE_COMMAND) + .pipe(concatMap(() => this.popupViewCacheState.update(() => null))) + .subscribe(); + merge( // on tab changed, excluding extension tabs fromChromeEvent(chrome.tabs.onActivated).pipe( diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts similarity index 57% rename from apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts rename to apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts index a2f0c78cd52..ca41127407c 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import type { BitwardenClient } from "@bitwarden/sdk-internal"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { BrowserApi } from "../../browser/browser-api"; +export type GlobalWithWasmInit = typeof globalThis & { + initSdk: () => void; +}; + // https://stackoverflow.com/a/47880734 const supported = (() => { try { @@ -17,7 +18,7 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } - } catch (e) { + } catch { // ignore } return false; @@ -31,54 +32,42 @@ let loadingPromise: Promise | undefined; if (BrowserApi.isManifestVersion(3)) { if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); loadingPromise = import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); loadingPromise = import("./fallback"); } } // Manifest v2 expects dynamic imports to prevent timing issues. -async function load() { +async function importModule(): Promise { if (BrowserApi.isManifestVersion(3)) { // Ensure we have loaded the module await loadingPromise; - return; - } - - if (supported) { + } else if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); await import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); await import("./fallback"); } + + // the wasm and fallback imports mutate globalThis to add the initSdk function + return (globalThis as GlobalWithWasmInit).initSdk; } -/** - * SDK client factory with a js fallback for when WASM is not supported. - * - * Works both in popup and service worker. - */ -export class BrowserSdkClientFactory implements SdkClientFactory { - constructor(private logService: LogService) {} +export class BrowserSdkLoadService implements SdkLoadService { + constructor(readonly logService: LogService) {} - async createSdkClient( - ...args: ConstructorParameters - ): Promise { + async load(): Promise { const startTime = performance.now(); - await load(); - + await importModule().then((initSdk) => initSdk()); const endTime = performance.now(); - const instance = (globalThis as any).init_sdk(...args); - - this.logService.info("WASM SDK loaded in", Math.round(endTime - startTime), "ms"); - - return instance; + this.logService.info(`WASM SDK loaded in ${Math.round(endTime - startTime)}ms`); } } diff --git a/apps/browser/src/platform/services/sdk/fallback.ts b/apps/browser/src/platform/services/sdk/fallback.ts index 82d292fc9ee..cee3598feda 100644 --- a/apps/browser/src/platform/services/sdk/fallback.ts +++ b/apps/browser/src/platform/services/sdk/fallback.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/platform/services/sdk/wasm.ts b/apps/browser/src/platform/services/sdk/wasm.ts index 1977a171e23..de2eeffd294 100644 --- a/apps/browser/src/platform/services/sdk/wasm.ts +++ b/apps/browser/src/platform/services/sdk/wasm.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8e48104737a..9473dc63bad 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,23 +1,23 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { EnvironmentSelectorComponent, EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, + activeAuthGuard, redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { @@ -41,6 +41,8 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; @@ -71,14 +73,10 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; -import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; -import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component"; -import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; -import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; @@ -148,11 +146,12 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { + { path: "fido2", + component: Fido2Component, canActivate: [fido2AuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, ...twofactorRefactorSwap( TwoFactorComponent, AnonLayoutWrapperComponent, @@ -178,12 +177,12 @@ const routes: Routes = [ component: ExtensionAnonLayoutWrapperComponent, children: [ { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn(unauthRouteOverrides)], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, ], data: { @@ -236,6 +235,27 @@ const routes: Routes = [ ], }, ), + { + path: "device-verification", + component: ExtensionAnonLayoutWrapperComponent, + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [{ path: "", component: NewDeviceVerificationComponent }], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + }, { path: "set-password", component: SetPasswordComponent, @@ -321,22 +341,24 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { + { path: "autofill", + component: AutofillComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "account-security", component: AccountSecurityComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { + { path: "notifications", + component: NotificationsSettingsComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "vault-settings", component: VaultSettingsV2Component, @@ -355,11 +377,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { + { path: "excluded-domains", + component: ExcludedDomainsComponent, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, - }), + }, { path: "premium", component: PremiumV2Component, @@ -560,7 +583,7 @@ const routes: Routes = [ children: [ { path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { elevation: 1, pageIcon: RegistrationUserAddIcon, @@ -586,7 +609,7 @@ const routes: Routes = [ }, { path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationLockAltIcon, elevation: 1, @@ -636,7 +659,6 @@ const routes: Routes = [ children: [ { path: "set-password-jit", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { pageTitle: { diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 8e51152be2e..8147524d2f3 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -21,6 +21,7 @@ import { ToastOptions, ToastService, } from "@bitwarden/components"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; @@ -64,6 +65,8 @@ export class AppComponent implements OnInit, OnDestroy { private toastService: ToastService, private accountService: AccountService, private animationControlService: AnimationControlService, + private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, ) {} async ngOnInit() { @@ -100,7 +103,7 @@ export class AppComponent implements OnInit, OnDestroy { this.messageListener.allMessages$ .pipe( - tap((msg: any) => { + tap(async (msg: any) => { if (msg.command === "doneLoggingOut") { // TODO: PM-8544 - why do we call logout in the popup after receiving the doneLoggingOut message? Hasn't this already completeted logout? this.authService.logOut(async () => { @@ -117,6 +120,7 @@ export class AppComponent implements OnInit, OnDestroy { msg.command === "locked" && (msg.userId == null || msg.userId == this.activeUserId) ) { + await this.biometricsService.setShouldAutopromptNow(false); // 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.router.navigate(["lock"]); @@ -124,16 +128,24 @@ export class AppComponent implements OnInit, OnDestroy { // 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.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { + } else if (msg.command === "showNativeMessagingFingerprintDialog") { // TODO: Should be refactored to live in another service. // 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.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showUpdateDesktopAppOrDisableFingerprintDialog") { + // TODO: Should be refactored to live in another service. + await this.showDialog({ + title: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogTitle"), + content: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogMessage"), + type: "warning", + }); } else if (msg.command === "showToast") { this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { if (this.platformUtilsService.isSafari()) { - window.setTimeout(() => { + window.setTimeout(async () => { + await this.biometricStateService.updateLastProcessReload(); window.location.reload(); }, 2000); } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 1fe8f1f18db..15a898aef53 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,17 +35,7 @@ import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; -import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; -import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; -import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; -import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; -import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; -import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; -import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; -import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; -import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; -import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; @@ -87,10 +77,6 @@ import "../platform/popup/locales"; ScrollingModule, ServicesModule, DialogModule, - ExcludedDomainsComponent, - Fido2CipherRowComponent, - Fido2Component, - Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, @@ -112,15 +98,11 @@ import "../platform/popup/locales"; ColorPasswordPipe, ColorPasswordCountPipe, EnvironmentComponent, - ExcludedDomainsV1Component, - Fido2CipherRowV1Component, - Fido2UseBrowserLinkV1Component, HintComponent, HomeComponent, LoginViaAuthRequestComponentV1, LoginComponentV1, LoginDecryptionOptionsComponentV1, - NotificationsSettingsV1Component, RegisterComponent, SetPasswordComponent, SsoComponentV1, @@ -131,8 +113,6 @@ import "../platform/popup/locales"; UserVerificationComponent, VaultTimeoutInputComponent, RemovePasswordComponent, - Fido2V1Component, - AutofillV1Component, EnvironmentSelectorComponent, ], exports: [], diff --git a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts index c860ef1e342..2a5a56e7dfc 100644 --- a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts +++ b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts @@ -1,7 +1,9 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { filter, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { MessageListener } from "@bitwarden/common/platform/messaging"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; export type DesktopSyncVerificationDialogParams = { @@ -13,8 +15,30 @@ export type DesktopSyncVerificationDialogParams = { standalone: true, imports: [JslibModule, ButtonModule, DialogModule], }) -export class DesktopSyncVerificationDialogComponent { - constructor(@Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams) {} +export class DesktopSyncVerificationDialogComponent implements OnDestroy, OnInit { + private destroy$ = new Subject(); + + constructor( + @Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams, + private dialogRef: DialogRef, + private messageListener: MessageListener, + ) {} + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngOnInit(): void { + this.messageListener.allMessages$ + .pipe( + filter((m) => m.command === "hideNativeMessagingFingerprintDialog"), + takeUntil(this.destroy$), + ) + .subscribe(() => { + this.dialogRef.close(); + }); + } static open(dialogService: DialogService, data: DesktopSyncVerificationDialogParams) { return dialogService.open(DesktopSyncVerificationDialogComponent, { diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index 8ffe0743bf6..dadd7917b99 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -4,7 +4,9 @@ import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { PopupSizeService } from "../platform/popup/layout/popup-size.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./scss/popup.scss"); +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./scss/tailwind.css"); import { AppModule } from "./app.module"; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 24661438495..069ebf4020d 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,6 +6,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -22,11 +23,13 @@ export class InitService { private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b67..67ed8391de3 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,8 +31,8 @@ import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarde import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { AccountService, AccountService as AccountServiceAbstraction, @@ -70,6 +70,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, @@ -82,6 +83,7 @@ import { flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -144,7 +146,7 @@ import BrowserMemoryStorageService from "../../platform/services/browser-memory- import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; -import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../../platform/services/sdk/browser-sdk-load.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; @@ -369,7 +371,7 @@ const safeProviders: SafeProvider[] = [ provide: VaultFilterService, useClass: VaultFilterService, deps: [ - OrganizationService, + DefaultOrganizationService, FolderServiceAbstraction, CipherService, CollectionService, @@ -566,11 +568,16 @@ const safeProviders: SafeProvider[] = [ deps: [MessageSender, MessageListener], }), safeProvider({ - provide: SdkClientFactory, - useFactory: (logService: LogService) => - flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(), + provide: SdkLoadService, + useClass: BrowserSdkLoadService, deps: [LogService], }), + safeProvider({ + provide: SdkClientFactory, + useFactory: () => + flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(), + deps: [], + }), safeProvider({ provide: LoginEmailService, useClass: LoginEmailService, diff --git a/apps/browser/src/services/families-policy.service.spec.ts b/apps/browser/src/services/families-policy.service.spec.ts index 19291bcd825..65a861038bf 100644 --- a/apps/browser/src/services/families-policy.service.spec.ts +++ b/apps/browser/src/services/families-policy.service.spec.ts @@ -6,6 +6,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { FamiliesPolicyService } from "./families-policy.service"; // Adjust the import as necessary @@ -13,16 +17,20 @@ describe("FamiliesPolicyService", () => { let service: FamiliesPolicyService; let organizationService: MockProxy; let policyService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); policyService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ providers: [ FamiliesPolicyService, { provide: OrganizationService, useValue: organizationService }, { provide: PolicyService, useValue: policyService }, + { provide: AccountService, useValue: accountService }, ], }); @@ -40,7 +48,7 @@ describe("FamiliesPolicyService", () => { jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true)); const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: true }] as Policy[]; policyService.getAll$.mockReturnValue(of(policies)); @@ -53,7 +61,7 @@ describe("FamiliesPolicyService", () => { jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true)); const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: false }] as Policy[]; policyService.getAll$.mockReturnValue(of(policies)); @@ -64,7 +72,7 @@ describe("FamiliesPolicyService", () => { it("should return true when there is exactly one enterprise organization that can manage sponsorships", async () => { const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const result = await firstValueFrom(service.hasSingleEnterpriseOrg$()); expect(result).toBe(true); @@ -75,7 +83,7 @@ describe("FamiliesPolicyService", () => { { id: "org1", canManageSponsorships: true }, { id: "org2", canManageSponsorships: true }, ] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const result = await firstValueFrom(service.hasSingleEnterpriseOrg$()); expect(result).toBe(false); diff --git a/apps/browser/src/services/families-policy.service.ts b/apps/browser/src/services/families-policy.service.ts index 426f39dcfd0..755d3e84591 100644 --- a/apps/browser/src/services/families-policy.service.ts +++ b/apps/browser/src/services/families-policy.service.ts @@ -4,27 +4,34 @@ import { map, Observable, of, switchMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; @Injectable({ providedIn: "root" }) export class FamiliesPolicyService { constructor( private policyService: PolicyService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} hasSingleEnterpriseOrg$(): Observable { // Retrieve all organizations the user is part of - return this.organizationService.getAll$().pipe( - map((organizations) => { - // Filter to only those organizations that can manage sponsorships - const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships); + return getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe( + map((organizations) => { + // Filter to only those organizations that can manage sponsorships + const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships); - // Check if there is exactly one organization that can manage sponsorships. - // This is important because users that are part of multiple organizations - // may always access free bitwarden family menu. We want to restrict access - // to the policy only when there is a single enterprise organization and the free family policy is turn. - return sponsorshipOrgs.length === 1; - }), + // Check if there is exactly one organization that can manage sponsorships. + // This is important because users that are part of multiple organizations + // may always access free bitwarden family menu. We want to restrict access + // to the policy only when there is a single enterprise organization and the free family policy is turn. + return sponsorshipOrgs.length === 1; + }), + ), + ), ); } @@ -34,18 +41,22 @@ export class FamiliesPolicyService { if (!hasSingleEnterpriseOrg) { return of(false); } - return this.organizationService.getAll$().pipe( - map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), - switchMap((enterpriseOrgId) => - this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - policies.find((policy) => policy.organizationId === enterpriseOrgId)?.enabled ?? - false, - ), + return getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe( + map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), + switchMap((enterpriseOrgId) => + this.policyService + .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) + .pipe( + map( + (policies) => + policies.find((policy) => policy.organizationId === enterpriseOrgId) + ?.enabled ?? false, + ), + ), ), + ), ), ); }), diff --git a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts index 6270b3503b5..0d49595d2eb 100644 --- a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts +++ b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/src/abstractions/vault-timeout/vault-timeout.service"; -import { MessagingService } from "@bitwarden/common/src/platform/abstractions/messaging.service"; -import { UserId } from "@bitwarden/common/src/types/guid"; +import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/common/types/guid"; export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { constructor(protected messagingService: MessagingService) {} diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts deleted file mode 100644 index 2ade5bf7672..00000000000 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; - -type FilelessImportPortMessage = { - command?: string; - importType?: FilelessImportTypeKeys; - data?: string; -}; - -type FilelessImportPortMessageHandlerParams = { - message: FilelessImportPortMessage; - port: chrome.runtime.Port; -}; - -type ImportNotificationMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - cancelFilelessImport: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; -}; - -type LpImporterMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - displayLpImportNotification: ({ port }: { port: chrome.runtime.Port }) => void; - startLpImport: ({ message }: { message: FilelessImportPortMessage }) => void; -}; - -interface FilelessImporterBackground { - init(): void; -} - -export { - FilelessImportPortMessage, - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground, -}; diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts deleted file mode 100644 index 429a0e12184..00000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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/vault/abstractions/sync/sync.service.abstraction"; -import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { - flushPromises, - sendPortMessage, - triggerRuntimeOnConnectEvent, -} from "../../autofill/spec/testing-utils"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; -import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; - -import FilelessImporterBackground from "./fileless-importer.background"; - -describe("FilelessImporterBackground ", () => { - let filelessImporterBackground: FilelessImporterBackground; - const configService = mock(); - const domainSettingsService = mock(); - const authService = mock(); - const policyService = mock(); - const notificationBackground = mock(); - const importService = mock(); - const syncService = mock(); - const platformUtilsService = mock(); - const logService = mock(); - let scriptInjectorService: BrowserScriptInjectorService; - let tabMock: chrome.tabs.Tab; - - beforeEach(() => { - domainSettingsService.blockedInteractionsUris$ = of(null); - policyService.policyAppliesToActiveUser$.mockImplementation(() => of(true)); - scriptInjectorService = new BrowserScriptInjectorService( - domainSettingsService, - platformUtilsService, - logService, - ); - filelessImporterBackground = new FilelessImporterBackground( - configService, - authService, - policyService, - notificationBackground, - importService, - syncService, - scriptInjectorService, - ); - filelessImporterBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sets up the port message listeners on initialization of the class", () => { - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function)); - }); - }); - - describe("handle ports onConnect", () => { - let lpImporterPort: chrome.runtime.Port; - let manifestVersionSpy: jest.SpyInstance; - let executeScriptInTabSpy: jest.SpyInstance; - - beforeEach(() => { - lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - tabMock = lpImporterPort.sender.tab; - jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock); - manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - jest.spyOn(filelessImporterBackground as any, "removeIndividualVault"); - }); - - it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => { - const port = createPortSpyMock("some-other-port"); - - triggerRuntimeOnConnectEvent(port); - await flushPromises(); - - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's auth status is not unlocked", async () => { - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Locked); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => { - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the feature flag is turned off", async () => { - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(false); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is enabled", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }); - }); - - it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - manifestVersionSpy.mockReturnValue(3); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith( - lpImporterPort.sender.tab.id, - { file: "content/lp-suppress-import-download.js", runAt: "document_start", frameId: 0 }, - { world: "MAIN" }, - ); - }); - - it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - manifestVersionSpy.mockReturnValue(2); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith(lpImporterPort.sender.tab.id, { - file: "content/lp-suppress-import-download-script-append-mv2.js", - runAt: "document_start", - frameId: 0, - }); - }); - }); - - describe("port messages", () => { - let notificationPort: chrome.runtime.Port; - let lpImporterPort: chrome.runtime.Port; - - beforeEach(async () => { - policyService.policyAppliesToActiveUser$.mockImplementation(() => of(false)); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar)); - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter)); - await flushPromises(); - notificationPort = filelessImporterBackground["importNotificationsPort"]; - lpImporterPort = filelessImporterBackground["lpImporterPort"]; - }); - - it("skips handling a message if a message handler is not associated with the port message command", () => { - sendPortMessage(notificationPort, { command: "commandNotFound" }); - - expect(chrome.tabs.sendMessage).not.toHaveBeenCalled(); - }); - - describe("import notification port messages", () => { - describe("startFilelessImport", () => { - it("sends a message to start the LastPass fileless import within the content script", () => { - sendPortMessage(notificationPort, { - command: "startFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "startLpFilelessImport", - }); - }); - }); - - describe("cancelFilelessImport", () => { - it("sends a message to close the notification bar", async () => { - sendPortMessage(notificationPort, { command: "cancelFilelessImport" }); - - expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( - notificationPort.sender.tab.id, - { - command: "closeNotificationBar", - }, - null, - expect.anything(), - ); - expect(lpImporterPort.postMessage).not.toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - }); - - it("sends a message to trigger a download of the LP importer CSV", () => { - sendPortMessage(notificationPort, { - command: "cancelFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - expect(lpImporterPort.disconnect).toHaveBeenCalled(); - }); - }); - }); - - describe("lp importer port messages", () => { - describe("displayLpImportNotification", () => { - it("creates a request fileless import notification", async () => { - jest.spyOn(filelessImporterBackground["notificationBackground"], "requestFilelessImport"); - - sendPortMessage(lpImporterPort, { - command: "displayLpImportNotification", - }); - await flushPromises(); - - expect( - filelessImporterBackground["notificationBackground"].requestFilelessImport, - ).toHaveBeenCalledWith(lpImporterPort.sender.tab, FilelessImportType.LP); - }); - }); - - describe("startLpImport", () => { - it("ignores the message if the message does not contain data", () => { - sendPortMessage(lpImporterPort, { - command: "startLpImport", - }); - - expect(filelessImporterBackground["importService"].import).not.toHaveBeenCalled(); - }); - - it("triggers the import of the LastPass vault", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest.spyOn(filelessImporterBackground["importService"], "import").mockResolvedValue( - mock({ - success: true, - }), - ); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect(filelessImporterBackground["importService"].import).toHaveBeenCalledWith( - importer, - data, - null, - null, - false, - ); - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportCompleted" }); - expect(filelessImporterBackground["syncService"].fullSync).toHaveBeenCalledWith(true); - }); - - it("posts a failed message if the import fails", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest - .spyOn(filelessImporterBackground["importService"], "import") - .mockImplementation(() => { - throw new Error("error"); - }); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportFailed" }); - }); - }); - }); - }); - - describe("handleImporterPortDisconnect", () => { - it("resets the port properties to null", () => { - const lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - const notificationPort = createPortSpyMock(FilelessImportPort.NotificationBar); - filelessImporterBackground["lpImporterPort"] = lpImporterPort; - filelessImporterBackground["importNotificationsPort"] = notificationPort; - - filelessImporterBackground["handleImporterPortDisconnect"](lpImporterPort); - - expect(filelessImporterBackground["lpImporterPort"]).toBeNull(); - expect(filelessImporterBackground["importNotificationsPort"]).not.toBeNull(); - - filelessImporterBackground["handleImporterPortDisconnect"](notificationPort); - - expect(filelessImporterBackground["importNotificationsPort"]).toBeNull(); - }); - }); -}); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts deleted file mode 100644 index 21d597ec8ae..00000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ /dev/null @@ -1,265 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; -import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; -import { - FilelessImportPort, - FilelessImportType, - FilelessImportTypeKeys, -} from "../enums/fileless-import.enums"; - -import { - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground as FilelessImporterBackgroundInterface, - FilelessImportPortMessage, -} from "./abstractions/fileless-importer.background"; - -class FilelessImporterBackground implements FilelessImporterBackgroundInterface { - private static readonly filelessImporterPortNames: Set = new Set([ - FilelessImportPort.LpImporter, - FilelessImportPort.NotificationBar, - ]); - private importNotificationsPort: chrome.runtime.Port; - private lpImporterPort: chrome.runtime.Port; - private readonly importNotificationsPortMessageHandlers: ImportNotificationMessageHandlers = { - startFilelessImport: ({ message }) => this.startFilelessImport(message.importType), - cancelFilelessImport: ({ message, port }) => - this.cancelFilelessImport(message.importType, port.sender), - }; - private readonly lpImporterPortMessageHandlers: LpImporterMessageHandlers = { - displayLpImportNotification: ({ port }) => - this.displayFilelessImportNotification(port.sender.tab, FilelessImportType.LP), - startLpImport: ({ message }) => this.triggerLpImport(message.data), - }; - - /** - * Creates a new instance of the fileless importer background logic. - * - * @param configService - Identifies if the feature flag is enabled. - * @param authService - Verifies if the auth status of the user. - * @param policyService - Identifies if the user account has a policy that disables personal ownership. - * @param notificationBackground - Used to inject the notification bar into the tab. - * @param importService - Used to import the export data into the vault. - * @param syncService - Used to trigger a full sync after the import is completed. - * @param scriptInjectorService - Used to inject content scripts that initialize the import process - */ - constructor( - private configService: ConfigService, - private authService: AuthService, - private policyService: PolicyService, - private notificationBackground: NotificationBackground, - private importService: ImportServiceAbstraction, - private syncService: SyncService, - private scriptInjectorService: ScriptInjectorService, - ) {} - - /** - * Initializes the fileless importer background logic. - */ - init() { - this.setupPortMessageListeners(); - } - - /** - * Starts an import of the export data pulled from the tab. - * - * @param importType - The type of import to start. Identifies the used content script. - */ - private startFilelessImport(importType: FilelessImportTypeKeys) { - if (importType === FilelessImportType.LP) { - this.lpImporterPort?.postMessage({ command: "startLpFilelessImport" }); - } - } - - /** - * Cancels an import of the export data pulled from the tab. This closes any - * existing notifications that are present in the tab, and triggers importer - * specific behavior based on the import type. - * - * @param importType - The type of import to cancel. Identifies the used content script. - * @param sender - The sender of the message. - */ - private async cancelFilelessImport( - importType: FilelessImportTypeKeys, - sender: chrome.runtime.MessageSender, - ) { - if (importType === FilelessImportType.LP) { - this.triggerLpImporterCsvDownload(); - } - - await BrowserApi.tabSendMessage(sender.tab, { command: "closeNotificationBar" }); - } - - /** - * Injects the notification bar into the passed tab. - * - * @param tab - * @param importType - */ - private async displayFilelessImportNotification(tab: chrome.tabs.Tab, importType: string) { - await this.notificationBackground.requestFilelessImport(tab, importType); - } - - /** - * Triggers the download of the CSV file from the LP importer. This is triggered - * when the user opts to not save the export to Bitwarden within the notification bar. - */ - private triggerLpImporterCsvDownload() { - this.lpImporterPort?.postMessage({ command: "triggerCsvDownload" }); - this.lpImporterPort?.disconnect(); - } - - /** - * Completes the import process for the LP importer. This is triggered when the - * user opts to save the export to Bitwarden within the notification bar. - * - * @param data - The export data to import. - * @param sender - The sender of the message. - */ - private async triggerLpImport(data: string) { - if (!data) { - return; - } - - const promptForPassword_callback = async () => ""; - const importer = this.importService.getImporter( - "lastpasscsv", - promptForPassword_callback, - null, - ); - - try { - const result = await this.importService.import(importer, data, null, null, false); - if (result.success) { - this.importNotificationsPort?.postMessage({ command: "filelessImportCompleted" }); - await this.syncService.fullSync(true); - } - } catch (error) { - this.importNotificationsPort?.postMessage({ - command: "filelessImportFailed", - importErrorMessage: Object.values(error).length - ? error - : chrome.i18n.getMessage("importNetworkError"), - }); - } - } - - /** - * Identifies if the user account has a policy that disables personal ownership. - */ - private async removeIndividualVault(): Promise { - return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ); - } - - /** - * Sets up onConnect listeners for the extension. - */ - private setupPortMessageListeners() { - chrome.runtime.onConnect.addListener(this.handlePortOnConnect); - } - - /** - * Handles connections from content scripts that affect the fileless importer behavior. - * Is used to facilitate the passing of data and user actions to enact the import - * of web content to the Bitwarden vault. Along with this, a check is made to ensure - * that the feature flag is enabled and the user is authenticated. - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - if (!FilelessImporterBackground.filelessImporterPortNames.has(port.name)) { - return; - } - - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.BrowserFilelessImport, - ); - const userAuthStatus = await this.authService.getAuthStatus(); - const removeIndividualVault = await this.removeIndividualVault(); - const filelessImportEnabled = - filelessImportFeatureFlagEnabled && - userAuthStatus === AuthenticationStatus.Unlocked && - !removeIndividualVault; - port.postMessage({ command: "verifyFeatureFlag", filelessImportEnabled }); - - if (!filelessImportEnabled) { - return; - } - - port.onMessage.addListener(this.handleImporterPortMessage); - port.onDisconnect.addListener(this.handleImporterPortDisconnect); - - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = port; - await this.scriptInjectorService.inject({ - tabId: port.sender.tab.id, - injectDetails: { runAt: "document_start" }, - mv2Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, - mv3Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3, - }); - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = port; - break; - } - }; - - /** - * Handles messages that are sent from fileless importer content scripts. - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handleImporterPortMessage = ( - message: FilelessImportPortMessage, - port: chrome.runtime.Port, - ) => { - let handler: CallableFunction | undefined; - - switch (port.name) { - case FilelessImportPort.LpImporter: - handler = this.lpImporterPortMessageHandlers[message.command]; - break; - case FilelessImportPort.NotificationBar: - handler = this.importNotificationsPortMessageHandlers[message.command]; - break; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; - - /** - * Handles disconnections from fileless importer content scripts. - * @param port - The port that was disconnected. - */ - private handleImporterPortDisconnect = (port: chrome.runtime.Port) => { - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = null; - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = null; - break; - } - }; -} - -export default FilelessImporterBackground; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts deleted file mode 100644 index 898ee1205ab..00000000000 --- a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Mv2ScriptInjectionDetails, - Mv3ScriptInjectionDetails, -} from "../../platform/services/abstractions/script-injector.service"; - -type FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: Mv2ScriptInjectionDetails; - mv3: Mv3ScriptInjectionDetails; - }; -}; - -const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: { - file: "content/lp-suppress-import-download-script-append-mv2.js", - }, - mv3: { - file: "content/lp-suppress-import-download.js", - world: "MAIN", - }, - }, -} as const; - -export { FilelessImporterInjectedScriptsConfig }; diff --git a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts b/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts deleted file mode 100644 index 018ea2c8d94..00000000000 --- a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts +++ /dev/null @@ -1,25 +0,0 @@ -type LpFilelessImporterMessage = { - command?: string; - data?: string; - filelessImportEnabled?: boolean; -}; - -type LpFilelessImporterMessageHandlerParams = { - message: LpFilelessImporterMessage; - port: chrome.runtime.Port; -}; - -type LpFilelessImporterMessageHandlers = { - [key: string]: ({ message, port }: LpFilelessImporterMessageHandlerParams) => void; - verifyFeatureFlag: ({ message }: { message: LpFilelessImporterMessage }) => void; - triggerCsvDownload: () => void; - startLpFilelessImport: () => void; -}; - -interface LpFilelessImporter { - init(): void; - handleFeatureFlagVerification(message: LpFilelessImporterMessage): void; - triggerCsvDownload(): void; -} - -export { LpFilelessImporterMessage, LpFilelessImporterMessageHandlers, LpFilelessImporter }; diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts deleted file mode 100644 index 432754ab91c..00000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { sendPortMessage } from "../../autofill/spec/testing-utils"; -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { LpFilelessImporter } from "./abstractions/lp-fileless-importer"; - -describe("LpFilelessImporter", () => { - let lpFilelessImporter: LpFilelessImporter & { [key: string]: any }; - const portSpy: chrome.runtime.Port = createPortSpyMock(FilelessImportPort.LpImporter); - chrome.runtime.connect = jest.fn(() => portSpy); - - beforeEach(() => { - require("./lp-fileless-importer"); - lpFilelessImporter = (globalThis as any).lpFilelessImporter; - }); - - afterEach(() => { - (globalThis as any).lpFilelessImporter = undefined; - jest.clearAllMocks(); - jest.resetModules(); - Object.defineProperty(document, "readyState", { - value: "complete", - writable: true, - }); - }); - - describe("init", () => { - it("sets up the port connection with the background script", () => { - lpFilelessImporter.init(); - - expect(chrome.runtime.connect).toHaveBeenCalledWith({ - name: FilelessImportPort.LpImporter, - }); - }); - }); - - describe("handleFeatureFlagVerification", () => { - it("disconnects the message port when the fileless import feature is disabled", () => { - lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: false }); - - expect(portSpy.disconnect).toHaveBeenCalled(); - }); - - it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => { - Object.defineProperty(document, "readyState", { - value: "loading", - writable: true, - }); - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(document.addEventListener).toHaveBeenCalledWith( - "DOMContentLoaded", - (lpFilelessImporter as any).loadImporter, - ); - }); - - it("sets up a mutation observer to watch the document body for injection of the export content", () => { - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - jest.spyOn(window, "MutationObserver").mockImplementationOnce(() => mock()); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(window.MutationObserver).toHaveBeenCalledWith( - (lpFilelessImporter as any).handleMutation, - ); - expect((lpFilelessImporter as any).mutationObserver.observe).toHaveBeenCalledWith( - document.body, - { childList: true, subtree: true }, - ); - }); - }); - - describe("triggerCsvDownload", () => { - it("posts a window message that triggers the download of the LastPass export", () => { - jest.spyOn(globalThis, "postMessage"); - - lpFilelessImporter.triggerCsvDownload(); - - expect(globalThis.postMessage).toHaveBeenCalledWith( - { command: "triggerCsvDownload" }, - "https://lastpass.com", - ); - }); - }); - - describe("handleMutation", () => { - beforeEach(() => { - lpFilelessImporter["mutationObserver"] = mock({ disconnect: jest.fn() }); - jest.spyOn(portSpy, "postMessage"); - }); - - it("ignores mutations that contain empty records", () => { - lpFilelessImporter["handleMutation"]([]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes in the mutation", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes with a tagname of `pre`", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "div" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain any textContent", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "pre" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain the expected header content", () => { - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "some other content" }] }, - ]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("will store the export data, display the import notification, and disconnect the mutation observer when the export data is appended", () => { - const observerDisconnectSpy = jest.spyOn( - lpFilelessImporter["mutationObserver"], - "disconnect", - ); - - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "url,username,password" }] }, - ]); - - expect(lpFilelessImporter["exportData"]).toEqual("url,username,password"); - expect(portSpy.postMessage).toHaveBeenCalledWith({ command: "displayLpImportNotification" }); - expect(observerDisconnectSpy).toHaveBeenCalled(); - }); - }); - - describe("handlePortMessage", () => { - it("ignores messages that are not registered with the portMessageHandlers", () => { - const message = { command: "unknownCommand" }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification"); - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).not.toHaveBeenCalled(); - expect(lpFilelessImporter.triggerCsvDownload).not.toHaveBeenCalled(); - }); - - it("handles the port message that verifies the fileless import feature flag", () => { - const message = { command: "verifyFeatureFlag", filelessImportEnabled: true }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification").mockImplementation(); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).toHaveBeenCalledWith(message); - }); - - it("handles the port message that triggers the LastPass csv download", () => { - const message = { command: "triggerCsvDownload" }; - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.triggerCsvDownload).toHaveBeenCalled(); - }); - - describe("handles the port message that triggers the LastPass fileless import", () => { - beforeEach(() => { - jest.spyOn(lpFilelessImporter as any, "postPortMessage"); - }); - - it("skips the import of the export data is not populated", () => { - const message = { command: "startLpFilelessImport" }; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).not.toHaveBeenCalled(); - }); - - it("starts the last pass fileless import", () => { - const message = { command: "startLpFilelessImport" }; - const exportData = "url,username,password"; - lpFilelessImporter["exportData"] = exportData; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).toHaveBeenCalledWith({ - command: "startLpImport", - data: exportData, - }); - }); - }); - }); -}); diff --git a/apps/browser/src/tools/content/lp-fileless-importer.ts b/apps/browser/src/tools/content/lp-fileless-importer.ts deleted file mode 100644 index 497a499b337..00000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.ts +++ /dev/null @@ -1,158 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { - LpFilelessImporter as LpFilelessImporterInterface, - LpFilelessImporterMessage, - LpFilelessImporterMessageHandlers, -} from "./abstractions/lp-fileless-importer"; - -class LpFilelessImporter implements LpFilelessImporterInterface { - private exportData: string; - private messagePort: chrome.runtime.Port; - private mutationObserver: MutationObserver; - private readonly portMessageHandlers: LpFilelessImporterMessageHandlers = { - verifyFeatureFlag: ({ message }) => this.handleFeatureFlagVerification(message), - triggerCsvDownload: () => this.triggerCsvDownload(), - startLpFilelessImport: () => this.startLpImport(), - }; - - /** - * Initializes the LP fileless importer. - */ - init() { - this.setupMessagePort(); - } - - /** - * Enacts behavior based on the feature flag verification message. If the feature flag is - * not enabled, the message port is disconnected. If the feature flag is enabled, the - * download of the CSV file is suppressed. - * - * @param message - The port message, contains the feature flag indicator. - */ - handleFeatureFlagVerification(message: LpFilelessImporterMessage) { - if (!message.filelessImportEnabled) { - this.messagePort?.disconnect(); - return; - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", this.loadImporter); - return; - } - - this.loadImporter(); - } - - /** - * Posts a message to the LP importer to trigger the download of the CSV file. - */ - triggerCsvDownload() { - this.postWindowMessage({ command: "triggerCsvDownload" }); - } - - /** - * Initializes the importing mechanism used to import the CSV file into Bitwarden. - * This is done by observing the DOM for the addition of the LP importer element. - */ - private loadImporter = () => { - this.mutationObserver = new MutationObserver(this.handleMutation); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true, - }); - }; - - /** - * Handles mutations that are observed by the mutation observer. When the exported data - * element is added to the DOM, the export data is extracted and the import prompt is - * displayed. - * - * @param mutations - The mutations that were observed. - */ - private handleMutation = (mutations: MutationRecord[]) => { - let textContent: string; - for (let index = 0; index < mutations?.length; index++) { - const mutation: MutationRecord = mutations[index]; - - textContent = Array.from(mutation.addedNodes) - .filter((node) => node.nodeName.toLowerCase() === "pre") - .map((node) => (node as HTMLPreElement).textContent?.trim()) - .find((text) => text?.indexOf("url,username,password") >= 0); - - if (textContent) { - break; - } - } - - if (textContent) { - this.exportData = textContent; - this.postPortMessage({ command: "displayLpImportNotification" }); - this.mutationObserver.disconnect(); - } - }; - - /** - * If the export data is present, sends a message to the background with - * the export data to start the import process. - */ - private startLpImport() { - if (!this.exportData) { - return; - } - - this.postPortMessage({ command: "startLpImport", data: this.exportData }); - this.messagePort?.disconnect(); - } - - /** - * Posts a message to the background script. - * - * @param message - The message to post. - */ - private postPortMessage(message: LpFilelessImporterMessage) { - this.messagePort?.postMessage(message); - } - - /** - * Posts a message to the global context of the page. - * - * @param message - The message to post. - */ - private postWindowMessage(message: LpFilelessImporterMessage) { - globalThis.postMessage(message, "https://lastpass.com"); - } - - /** - * Sets up the message port that is used to facilitate communication between the - * background script and the content script. - */ - private setupMessagePort() { - this.messagePort = chrome.runtime.connect({ name: FilelessImportPort.LpImporter }); - this.messagePort.onMessage.addListener(this.handlePortMessage); - } - - /** - * Handles messages that are sent from the background script. - * - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handlePortMessage = (message: LpFilelessImporterMessage, port: chrome.runtime.Port) => { - const handler = this.portMessageHandlers[message.command]; - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -(function () { - if (!(globalThis as any).lpFilelessImporter) { - (globalThis as any).lpFilelessImporter = new LpFilelessImporter(); - (globalThis as any).lpFilelessImporter.init(); - } -})(); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts deleted file mode 100644 index 95b49ea00ec..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -describe("LP Suppress Import Download for Manifest v2", () => { - it("appends the `lp-suppress-import-download.js` script to the document element", () => { - let createdScriptElement: HTMLScriptElement; - jest.spyOn(window.document, "createElement"); - jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => { - createdScriptElement = node as HTMLScriptElement; - return node; - }); - - require("./lp-suppress-import-download-script-append.mv2"); - - expect(window.document.createElement).toHaveBeenCalledWith("script"); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js"); - expect(window.document.documentElement.appendChild).toHaveBeenCalledWith( - expect.any(HTMLScriptElement), - ); - expect(createdScriptElement.src).toBe( - "chrome-extension://id/content/lp-suppress-import-download.js", - ); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts deleted file mode 100644 index cd641590ad1..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This script handles injection of the LP suppress import download script into the document. - * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. - */ -(function (globalContext) { - const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js"); - globalContext.document.documentElement.appendChild(script); -})(window); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts deleted file mode 100644 index bfff3787506..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils"; - -describe("LP Suppress Import Download", () => { - const downloadAttribute = "file.csv"; - const hrefAttribute = "https://example.com/file.csv"; - const overridenHrefAttribute = "javascript:void(0)"; - let anchor: HTMLAnchorElement; - - beforeEach(() => { - jest.spyOn(Element.prototype, "appendChild"); - jest.spyOn(window, "addEventListener"); - - require("./lp-suppress-import-download"); - - anchor = document.createElement("a"); - anchor.download = downloadAttribute; - anchor.href = hrefAttribute; - anchor.click = jest.fn(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - it("disables the automatic download anchor", () => { - document.body.appendChild(anchor); - - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - }); - - it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => { - window.document.createElement = jest.fn(() => anchor); - jest.spyOn(window, "removeEventListener"); - - document.body.appendChild(anchor); - - // Precondition - Ensure the anchor in the document has overridden href and download attributes - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalled(); - expect(anchor.href).toEqual(hrefAttribute); - expect(anchor.download).toEqual(downloadAttribute); - expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function)); - }); - - it("skips subsequent calls to trigger a CSVDownload", async () => { - window.document.createElement = jest.fn(() => anchor); - - document.body.appendChild(anchor); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalledTimes(1); - }); - - it("skips triggering the CSV download for window messages that do not have the correct command", () => { - document.body.appendChild(anchor); - - postWindowMessage({ command: "notTriggerCsvDownload" }); - - expect(anchor.click).not.toHaveBeenCalled(); - }); - - it("skips triggering the CSV download for window messages that do not have a data value", () => { - document.body.appendChild(anchor); - - postWindowMessage(null); - - expect(anchor.click).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.ts b/apps/browser/src/tools/content/lp-suppress-import-download.ts deleted file mode 100644 index 1d5d449d199..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.ts +++ /dev/null @@ -1,52 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -/** - * Handles intercepting the injection of the CSV download link, and ensures the - * download of the script is suppressed until the user opts to download the file. - * The download is triggered by a window message sent from the LpFilelessImporter - * content script. - */ -(function (globalContext) { - let csvDownload = ""; - let csvHref = ""; - let isCsvDownloadTriggered = false; - const defaultAppendChild = Element.prototype.appendChild; - Element.prototype.appendChild = function (newChild: Node) { - if (isAnchorElement(newChild) && newChild.download) { - csvDownload = newChild.download; - csvHref = newChild.href; - newChild.setAttribute("href", "javascript:void(0)"); - newChild.setAttribute("download", ""); - Element.prototype.appendChild = defaultAppendChild; - } - - return defaultAppendChild.call(this, newChild); - }; - - function isAnchorElement(node: Node): node is HTMLAnchorElement { - return node.nodeName.toLowerCase() === "a"; - } - - const handleWindowMessage = (event: MessageEvent) => { - const command = event.data?.command; - if ( - event.source !== globalContext || - command !== "triggerCsvDownload" || - isCsvDownloadTriggered - ) { - return; - } - - isCsvDownloadTriggered = true; - globalContext.removeEventListener("message", handleWindowMessage); - - const anchor = globalContext.document.createElement("a"); - anchor.setAttribute("href", csvHref); - anchor.setAttribute("download", csvDownload); - globalContext.document.body.appendChild(anchor); - anchor.click(); - globalContext.document.body.removeChild(anchor); - }; - - globalContext.addEventListener("message", handleWindowMessage); -})(window); diff --git a/apps/browser/src/tools/enums/fileless-import.enums.ts b/apps/browser/src/tools/enums/fileless-import.enums.ts deleted file mode 100644 index e20f4f15454..00000000000 --- a/apps/browser/src/tools/enums/fileless-import.enums.ts +++ /dev/null @@ -1,12 +0,0 @@ -const FilelessImportType = { - LP: "LP", -} as const; - -type FilelessImportTypeKeys = (typeof FilelessImportType)[keyof typeof FilelessImportType]; - -const FilelessImportPort = { - NotificationBar: "fileless-importer-notification-bar", - LpImporter: "lp-fileless-importer", -} as const; - -export { FilelessImportType, FilelessImportTypeKeys, FilelessImportPort }; diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts index 8b880e88671..20e5548f99c 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts @@ -6,6 +6,7 @@ import { Observable, firstValueFrom, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { DialogService, ItemModule } from "@bitwarden/components"; @@ -43,6 +44,9 @@ export class MoreFromBitwardenPageV2Component { private familiesPolicyService: FamiliesPolicyService, private accountService: AccountService, ) { + this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)), + ); this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => account @@ -50,7 +54,6 @@ export class MoreFromBitwardenPageV2Component { : of(false), ), ); - this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); } diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 7ff958e26ac..26aeea4f20a 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -8,38 +8,44 @@ - {{ "accountSecurity" | i18n }} + + + {{ "accountSecurity" | i18n }} - {{ "autofill" | i18n }} + + + {{ "autofill" | i18n }} - {{ "notifications" | i18n }} + + + {{ "notifications" | i18n }} - {{ "vault" | i18n }} + + + {{ "vault" | i18n }} - {{ "appearance" | i18n }} + + + {{ "appearance" | i18n }} - {{ "about" | i18n }} + + + {{ "about" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index a46f5a6955b..152c500d6ca 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -26,5 +26,15 @@ + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index ebfb1ff765f..3252f030fc3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { CipherFormConfig, @@ -40,7 +42,7 @@ describe("AddEditV2Component", () => { const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; const buildConfig = jest.fn((mode: CipherFormMode) => - Promise.resolve({ mode, ...buildConfigResponse }), + Promise.resolve({ ...buildConfigResponse, mode }), ); const queryParams$ = new BehaviorSubject({}); const disable = jest.fn(); @@ -55,9 +57,10 @@ describe("AddEditV2Component", () => { back.mockClear(); collect.mockClear(); - addEditCipherInfo$ = new BehaviorSubject(null); + addEditCipherInfo$ = new BehaviorSubject(null); cipherServiceMock = mock(); - cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + cipherServiceMock.addEditCipherInfo$ = + addEditCipherInfo$.asObservable() as Observable; await TestBed.configureTestingModule({ imports: [AddEditV2Component], @@ -71,6 +74,13 @@ describe("AddEditV2Component", () => { { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: CipherService, useValue: cipherServiceMock }, { provide: EventCollectionService, useValue: { collect } }, + { provide: LogService, useValue: mock() }, + { + provide: CipherAuthorizationService, + useValue: { + canDeleteCipher$: jest.fn().mockReturnValue(true), + }, + }, ], }) .overrideProvider(CipherFormConfigService, { @@ -92,7 +102,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(buildConfig.mock.lastCall![0]).toBe("add"); expect(component.config.mode).toBe("add"); })); @@ -101,7 +111,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(buildConfig.mock.lastCall![0]).toBe("clone"); expect(component.config.mode).toBe("clone"); })); @@ -111,7 +121,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("edit"); })); @@ -121,7 +131,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("partial-edit"); })); }); @@ -218,7 +228,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.username).toBe("identity-username"); + expect(component.config.initialValues!.username).toBe("identity-username"); })); it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { @@ -231,7 +241,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.name).toBe("AddEditCipherName"); + expect(component.config.initialValues!.name).toBe("AddEditCipherName"); })); it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { @@ -326,4 +336,30 @@ describe("AddEditV2Component", () => { expect(back).toHaveBeenCalled(); }); }); + + describe("delete", () => { + it("dialogService openSimpleDialog called when deleteBtn is hit", async () => { + const dialogSpy = jest + .spyOn(component["dialogService"], "openSimpleDialog") + .mockResolvedValue(true); + + await component.delete(); + expect(dialogSpy).toHaveBeenCalled(); + }); + + it("should call deleteCipher when user confirms deletion", async () => { + const deleteCipherSpy = jest.spyOn(component as any, "deleteCipher"); + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + await component.delete(); + expect(deleteCipherSpy).toHaveBeenCalled(); + }); + + it("navigates to vault tab after deletion", async () => { + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + await component.delete(); + + expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 2d8c4857c1c..b46b1d61509 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -5,18 +5,27 @@ import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom, map, switchMap } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; -import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + SearchModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { CipherFormConfig, CipherFormConfigService, @@ -131,11 +140,13 @@ export type AddEditQueryParams = Partial>; CipherFormModule, AsyncActionsModule, PopOutComponent, + IconButtonModule, ], }) export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; + canDeleteCipher$: Observable; get loading() { return this.config == null; @@ -165,6 +176,10 @@ export class AddEditV2Component implements OnInit { private router: Router, private cipherService: CipherService, private eventCollectionService: EventCollectionService, + private logService: LogService, + private toastService: ToastService, + private dialogService: DialogService, + protected cipherAuthorizationService: CipherAuthorizationService, ) { this.subscribeToParams(); } @@ -281,6 +296,10 @@ export class AddEditV2Component implements OnInit { } if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + config.originalCipher, + ); + await this.eventCollectionService.collect( EventType.Cipher_ClientViewed, config.originalCipher.id, @@ -337,6 +356,43 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); } } + + delete = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/tabs/vault"]); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.config.originalCipher.deletedDate + ? this.cipherService.deleteWithServer(this.config.originalCipher.id) + : this.cipherService.softDeleteWithServer(this.config.originalCipher.id); + } } /** diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 4f6c4aa07cf..66d9096cd5c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -50,7 +50,7 @@ describe("OpenAttachmentsComponent", () => { } as Organization; const getCipher = jest.fn().mockResolvedValue(cipherDomain); - const getOrganization = jest.fn().mockResolvedValue(org); + const organizations$ = jest.fn().mockReturnValue(of([org])); const showFilePopoutMessage = jest.fn().mockReturnValue(false); const mockUserId = Utils.newGuid() as UserId; @@ -67,7 +67,7 @@ describe("OpenAttachmentsComponent", () => { openCurrentPagePopout.mockClear(); getCipher.mockClear(); showToast.mockClear(); - getOrganization.mockClear(); + organizations$.mockClear(); showFilePopoutMessage.mockClear(); hasPremiumFromAnySource$.next(true); @@ -89,7 +89,7 @@ describe("OpenAttachmentsComponent", () => { }, { provide: OrganizationService, - useValue: { get: getOrganization }, + useValue: { organizations$ }, }, { provide: FilePopoutUtilsService, @@ -148,11 +148,11 @@ describe("OpenAttachmentsComponent", () => { describe("Free Orgs", () => { beforeEach(() => { - component.cipherIsAPartOfFreeOrg = undefined; + component.cipherIsAPartOfFreeOrg = false; }); it("sets `cipherIsAPartOfFreeOrg` to false when the cipher is not a part of an organization", async () => { - cipherView.organizationId = null; + cipherView.organizationId = ""; await component.ngOnInit(); @@ -162,6 +162,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Free; + org.id = cipherView.organizationId; await component.ngOnInit(); @@ -171,6 +172,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Families; + org.id = cipherView.organizationId; await component.ngOnInit(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 5e27ccd5c41..aca494716b1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -7,8 +7,12 @@ import { Router } from "@angular/router"; import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -86,7 +90,12 @@ export class OpenAttachmentsComponent implements OnInit { return; } - const org = await this.organizationService.get(cipher.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(cipher.organizationId)), + ); this.cipherIsAPartOfFreeOrg = org.productTierType === ProductTierType.Free; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 7c4ea3e5b46..eae8e2cc980 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -1,7 +1,7 @@ = + this.vaultPopupAutofillService.currentTabIsOnBlocklist$; + constructor( private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupAutofillService: VaultPopupAutofillService, diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html new file mode 100644 index 00000000000..05db600bd5a --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html @@ -0,0 +1,10 @@ + + {{ "autofillBlockedNoticeV2" | i18n }} + + {{ "autofillBlockedNoticeGuidance" | i18n }} + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts new file mode 100644 index 00000000000..3a17825f4fb --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts @@ -0,0 +1,53 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + BannerModule, + IconButtonModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; + +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; + +const blockedURISettingsRoute = "/blocked-domains"; + +@Component({ + standalone: true, + imports: [ + BannerModule, + CommonModule, + IconButtonModule, + JslibModule, + LinkModule, + RouterModule, + TypographyModule, + ], + selector: "blocked-injection-banner", + templateUrl: "blocked-injection-banner.component.html", +}) +export class BlockedInjectionBanner implements OnInit { + /** + * Flag indicating that the banner should be shown + */ + protected showCurrentTabIsBlockedBanner$: Observable = + this.vaultPopupAutofillService.showCurrentTabIsBlockedBanner$; + + /** + * Hostname for current tab + */ + protected currentTabHostname?: string; + + blockedURISettingsRoute: string = blockedURISettingsRoute; + + constructor(private vaultPopupAutofillService: VaultPopupAutofillService) {} + + async ngOnInit() {} + + async handleCurrentTabIsBlockedBannerDismiss() { + await this.vaultPopupAutofillService.dismissCurrentTabIsBlockedBanner(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 7f87f32fcd4..4c7067df53a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -5,6 +5,7 @@ size="small" [attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name" [title]="'moreOptionsTitle' | i18n: cipher.name" + [disabled]="cipher.decryptionFailure" [bitMenuTriggerFor]="moreOptions" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 5d3dee9018e..8634d680052 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -9,6 +9,7 @@ import { filter } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -88,7 +89,8 @@ export class ItemMoreOptionsComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.hasOrganizations = await this.organizationService.hasOrganizations(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.hasOrganizations = await firstValueFrom(this.organizationService.hasOrganizations(userId)); } get canEdit() { diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index be30b29fb18..d57b1d2fe36 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -50,7 +50,7 @@ export class NewItemDropdownV2Component implements OnInit { this.tab = await BrowserApi.getTabFromCurrentWindow(); } - async buildQueryParams(type: CipherType): Promise { + buildQueryParams(type: CipherType): AddEditQueryParams { const poppedOut = BrowserPopupUtils.inPopout(window); const loginDetails: { uri?: string; name?: string } = {}; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts index 38ec6056d19..1f67dd51c21 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts @@ -152,7 +152,7 @@ describe("VaultHeaderV2Component", () => { it("defaults the initial state to true", (done) => { // The initial value of the `state$` variable above is undefined component["initialDisclosureVisibility$"].subscribe((initialVisibility) => { - expect(initialVisibility).toBeTrue(); + expect(initialVisibility).toBe(true); done(); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts index 8ec04045e6c..3b9dc9a1647 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts @@ -8,6 +8,8 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DisclosureTriggerForDirective, IconButtonModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DisclosureComponent } from "../../../../../../../../libs/components/src/disclosure/disclosure.component"; import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator"; import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index b5d92a386b3..1593c747f7d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -1,23 +1,76 @@ -
- -

- {{ title }} -

- - {{ ciphers.length }} -
-
+ + + + + + + + +
+ +
+ + +
+
+ + + +

+ {{ title }} +

+ + + + {{ ciphers.length }} + + + + + +
+
+ +
{{ description }}
+
+ + - + - - - - `, - imports: [CommonModule, DialogModule, ButtonModule, JslibModule, IconModule], -}) -export class VaultUiOnboardingComponent { - icon = announcementIcon; - - static open(dialogService: DialogService) { - return dialogService.open(VaultUiOnboardingComponent); - } - - navigateToLink = async () => { - window.open( - "https://bitwarden.com/blog/bringing-intuitive-workflows-and-visual-updates-to-the-bitwarden-browser/", - "_blank", - ); - }; -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 4798ddf4dfb..43471e56e7b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -22,6 +22,11 @@ + +
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 9970c115bb7..7c21c7e6a0c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -1,15 +1,17 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; import { combineLatest, Observable, shareReplay, switchMap } from "rxjs"; +import { filter, map, take } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; -import { VaultIcons } from "@bitwarden/vault"; +import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components"; +import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; @@ -17,8 +19,8 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; -import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service"; +import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component"; import { NewItemDropdownV2Component, NewItemInitialValues, @@ -38,6 +40,7 @@ enum VaultState { templateUrl: "vault-v2.component.html", standalone: true, imports: [ + BlockedInjectionBanner, PopupPageComponent, PopupHeaderComponent, PopOutComponent, @@ -52,8 +55,8 @@ enum VaultState { NewItemDropdownV2Component, ScrollingModule, VaultHeaderV2Component, + DecryptionFailureDialogComponent, ], - providers: [VaultUiOnboardingService], }) export class VaultV2Component implements OnInit, OnDestroy { cipherType = CipherType; @@ -88,7 +91,9 @@ export class VaultV2Component implements OnInit, OnDestroy { constructor( private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupListFiltersService: VaultPopupListFiltersService, - private vaultUiOnboardingService: VaultUiOnboardingService, + private destroyRef: DestroyRef, + private cipherService: CipherService, + private dialogService: DialogService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, @@ -115,7 +120,18 @@ export class VaultV2Component implements OnInit, OnDestroy { } async ngOnInit() { - await this.vaultUiOnboardingService.showOnboardingDialog(); + this.cipherService.failedToDecryptCiphers$ + .pipe( + map((ciphers) => ciphers.filter((c) => !c.isDeleted)), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); } ngOnDestroy(): void {} diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 7ee15aa833b..526ab2e2579 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -179,7 +179,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(doAutofill).toHaveBeenCalledOnce(); + expect(doAutofill).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-username"', fakeAsync(() => { @@ -187,7 +187,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-password"', fakeAsync(() => { @@ -195,7 +195,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-totp"', fakeAsync(() => { @@ -203,7 +203,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it("closes the popout after a load action", fakeAsync(() => { @@ -218,9 +218,9 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(doAutofill).toHaveBeenCalledOnce(); + expect(doAutofill).toHaveBeenCalledTimes(1); expect(focusSpy).toHaveBeenCalledWith(99); - expect(closeSpy).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledTimes(1); })); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 55b59c087c5..f3cd713dd5f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -37,7 +37,11 @@ import { } from "@bitwarden/components"; import { CopyCipherFieldService } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; @@ -231,7 +235,11 @@ export class ViewV2Component { } protected showFooter(): boolean { - return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit)); + return ( + this.cipher && + (!this.cipher.isDeleted || + (this.cipher.isDeleted && this.cipher.edit && this.cipher.viewPassword)) + ); } /** diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts index 25a7d7594ae..2dad1e3034c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -4,6 +4,7 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -20,6 +21,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutoFillOptions, @@ -42,6 +45,7 @@ describe("VaultPopupAutofillService", () => { // Create mocks for VaultPopupAutofillService const mockAutofillService = mock(); + const mockDomainSettingsService = mock(); const mockI18nService = mock(); const mockToastService = mock(); const mockPlatformUtilsService = mock(); @@ -69,6 +73,7 @@ describe("VaultPopupAutofillService", () => { testBed = TestBed.configureTestingModule({ providers: [ { provide: AutofillService, useValue: mockAutofillService }, + { provide: DomainSettingsService, useValue: mockDomainSettingsService }, { provide: I18nService, useValue: mockI18nService }, { provide: ToastService, useValue: mockToastService }, { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index e4863d5ee43..ff282d7a6d0 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -15,6 +15,7 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -26,6 +27,8 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutofillService, @@ -65,6 +68,74 @@ export class VaultPopupAutofillService { shareReplay({ refCount: false, bufferSize: 1 }), ); + currentTabIsOnBlocklist$: Observable = combineLatest([ + this.domainSettingsService.blockedInteractionsUris$, + this.currentAutofillTab$, + ]).pipe( + map(([blockedInteractionsUris, currentTab]) => { + if (blockedInteractionsUris && currentTab?.url?.length) { + const tabURL = new URL(currentTab.url); + const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname); + + if (tabIsBlocked) { + return true; + } + } + + return false; + }), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + showCurrentTabIsBlockedBanner$: Observable = combineLatest([ + this.domainSettingsService.blockedInteractionsUris$, + this.currentAutofillTab$, + ]).pipe( + map(([blockedInteractionsUris, currentTab]) => { + if (blockedInteractionsUris && currentTab?.url?.length) { + const tabURL = new URL(currentTab.url); + const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname); + + const showScriptInjectionIsBlockedBanner = + tabIsBlocked && !blockedInteractionsUris[tabURL.hostname]?.bannerIsDismissed; + + return showScriptInjectionIsBlockedBanner; + } + + return false; + }), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + async dismissCurrentTabIsBlockedBanner() { + try { + const currentTab = await firstValueFrom(this.currentAutofillTab$); + const currentTabURL = currentTab?.url.length && new URL(currentTab.url); + + const currentTabHostname = currentTabURL && currentTabURL.hostname; + + if (!currentTabHostname) { + return; + } + + const blockedURIs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$); + const tabIsBlocked = Object.keys(blockedURIs).includes(currentTabHostname); + + if (tabIsBlocked) { + void this.domainSettingsService.setBlockedInteractionsUris({ + ...blockedURIs, + [currentTabHostname as string]: { bannerIsDismissed: true }, + }); + } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + throw new Error( + "There was a problem dismissing the blocked interaction URI notification banner", + ); + } + } + /** * Observable that indicates whether autofill is allowed in the current context. * Autofill is allowed when there is a current tab and the popup is not in a popout window. @@ -123,6 +194,7 @@ export class VaultPopupAutofillService { constructor( private autofillService: AutofillService, + private domainSettingsService: DomainSettingsService, private i18nService: I18nService, private toastService: ToastService, private platformUtilService: PlatformUtilsService, diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 5b0eb63998d..7b241a6c108 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -6,15 +6,19 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { ObservableTracker } from "@bitwarden/common/spec"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -41,6 +45,8 @@ describe("VaultPopupItemsService", () => { const vaultAutofillServiceMock = mock(); const syncServiceMock = mock(); const inlineMenuFieldQualificationServiceMock = mock(); + const userId = Utils.newGuid() as UserId; + const accountServiceMock = mockAccountServiceWith(userId); beforeEach(() => { allCiphers = cipherFactory(10); @@ -58,6 +64,7 @@ describe("VaultPopupItemsService", () => { cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); cipherServiceMock.ciphers$ = new BehaviorSubject(null); cipherServiceMock.localData$ = new BehaviorSubject(null); + cipherServiceMock.failedToDecryptCiphers$ = new BehaviorSubject([]); searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => ciphers.filter((c) => ["0", "1"].includes(c.id)), @@ -96,7 +103,7 @@ describe("VaultPopupItemsService", () => { { id: "col2", name: "Collection 2" } as CollectionView, ]; - organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); + organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg])); collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); activeUserLastSync$ = new BehaviorSubject(new Date()); @@ -108,6 +115,7 @@ describe("VaultPopupItemsService", () => { { provide: VaultSettingsService, useValue: vaultSettingsServiceMock }, { provide: SearchService, useValue: searchService }, { provide: OrganizationService, useValue: organizationServiceMock }, + { provide: AccountService, useValue: accountServiceMock }, { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, @@ -294,21 +302,6 @@ describe("VaultPopupItemsService", () => { }); }); - it("should sort by last used then by name by default", (done) => { - service.remainingCiphers$.subscribe(() => { - expect(cipherServiceMock.getLocaleSortingFunction).toHaveBeenCalled(); - done(); - }); - }); - - it("should NOT sort by last used then by name when search text is applied", (done) => { - service.applyFilter("Login"); - service.remainingCiphers$.subscribe(() => { - expect(cipherServiceMock.getLocaleSortingFunction).not.toHaveBeenCalled(); - done(); - }); - }); - it("should filter remainingCiphers$ down to search term", (done) => { const cipherList = Object.values(allCiphers); const searchText = "Login"; @@ -375,20 +368,17 @@ describe("VaultPopupItemsService", () => { }); describe("deletedCiphers$", () => { - it("should return deleted ciphers", (done) => { - const ciphers = [ - { id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true }, - { id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true }, - { id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true }, - { id: "4", type: CipherType.Login, name: "Login 4", isDeleted: false }, - ] as CipherView[]; + it("should return deleted ciphers", async () => { + const deletedCipher = new CipherView(); + deletedCipher.deletedDate = new Date(); + const ciphers = [new CipherView(), new CipherView(), new CipherView(), deletedCipher]; cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers); - service.deletedCiphers$.subscribe((deletedCiphers) => { - expect(deletedCiphers.length).toBe(3); - done(); - }); + (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + + const deletedCiphers = await firstValueFrom(service.deletedCiphers$); + expect(deletedCiphers.length).toBe(1); }); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 93aa8cdaba9..519b8e6867f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -24,6 +24,7 @@ import { import { CollectionService } from "@bitwarden/admin-console/common"; 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; @@ -56,6 +57,9 @@ export class VaultPopupItemsService { latestSearchText$: Observable = this._searchText$.asObservable(); + private organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ); /** * Observable that contains the list of other cipher types that should be shown * in the autofill section of the Vault tab. Depends on vault settings. @@ -67,9 +71,9 @@ export class VaultPopupItemsService { this.vaultPopupAutofillService.nonLoginCipherTypesOnPage$, ]).pipe( map(([showCardsSettingEnabled, showIdentitiesSettingEnabled, nonLoginCipherTypesOnPage]) => { - const showCards = showCardsSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Card]; + const showCards = showCardsSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Card]; const showIdentities = - showIdentitiesSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Identity]; + showIdentitiesSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Identity]; return [ ...(showCards ? [CipherType.Card] : []), @@ -90,15 +94,14 @@ export class VaultPopupItemsService { tap(() => this._ciphersLoading$.next()), waitUntilSync(this.syncService), switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), + withLatestFrom(this.cipherService.failedToDecryptCiphers$), + map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]), shareReplay({ refCount: true, bufferSize: 1 }), ); private _activeCipherList$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -190,11 +193,6 @@ export class VaultPopupItemsService { (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), ), ), - withLatestFrom(this._hasSearchText$), - map(([ciphers, hasSearchText]) => - // Do not sort alphabetically when there is search text, default to the search service scoring - hasSearchText ? ciphers : ciphers.sort(this.cipherService.getLocaleSortingFunction()), - ), shareReplay({ refCount: false, bufferSize: 1 }), ); @@ -235,7 +233,7 @@ export class VaultPopupItemsService { /** Observable that indicates when the user should see the deactivated org state */ showDeactivatedOrg$: Observable = combineLatest([ this.vaultPopupListFiltersService.filters$.pipe(distinctUntilKeyChanged("organization")), - this.organizationService.organizations$, + this.organizations$, ]).pipe( map(([filters, orgs]) => { if (!filters.organization || filters.organization.id === MY_VAULT_ID) { @@ -250,8 +248,25 @@ export class VaultPopupItemsService { /** * Observable that contains the list of ciphers that have been deleted. */ - deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( - map((ciphers) => ciphers.filter((c) => c.isDeleted)), + deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( + switchMap((ciphers) => + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( + map(([organizations, collections]) => { + const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); + const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); + return ciphers + .filter((c) => c.isDeleted) + .map( + (cipher) => + new PopupCipherView( + cipher, + cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), + orgMap[cipher.organizationId as OrganizationId], + ), + ); + }), + ), + ), shareReplay({ refCount: false, bufferSize: 1 }), ); @@ -264,6 +279,7 @@ export class VaultPopupItemsService { private collectionService: CollectionService, private vaultPopupAutofillService: VaultPopupAutofillService, private syncService: SyncService, + private accountService: AccountService, ) {} applyFilter(newSearchText: string) { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 0eb91c6cbe2..7f570e8f5c9 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -23,7 +23,9 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi describe("VaultPopupListFiltersService", () => { let service: VaultPopupListFiltersService; - const memberOrganizations$ = new BehaviorSubject([]); + const _memberOrganizations$ = new BehaviorSubject([]); + const memberOrganizations$ = (userId: UserId) => _memberOrganizations$; + const organizations$ = new BehaviorSubject([]); const folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); const decryptedCollections$ = new BehaviorSubject([]); @@ -44,6 +46,7 @@ describe("VaultPopupListFiltersService", () => { const organizationService = { memberOrganizations$, + organizations$, } as unknown as OrganizationService; const i18nService = { @@ -58,7 +61,7 @@ describe("VaultPopupListFiltersService", () => { const update = jest.fn().mockResolvedValue(undefined); beforeEach(() => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); decryptedCollections$.next([]); policyAppliesToActiveUser$.next(false); policyService.policyAppliesToActiveUser$.mockClear(); @@ -135,7 +138,7 @@ describe("VaultPopupListFiltersService", () => { describe("organizations$", () => { it('does not add "myVault" to the list of organizations when there are no organizations', (done) => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([]); @@ -145,7 +148,7 @@ describe("VaultPopupListFiltersService", () => { it('adds "myVault" to the list of organizations when there are other organizations', (done) => { const orgs = [{ name: "bobby's org", id: "1234-3323-23223" }] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual(["myVault", "bobby's org"]); @@ -158,7 +161,7 @@ describe("VaultPopupListFiltersService", () => { { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -179,7 +182,7 @@ describe("VaultPopupListFiltersService", () => { it("returns an empty array when the policy applies and there is a single organization", (done) => { policyAppliesToActiveUser$.next(true); - memberOrganizations$.next([ + _memberOrganizations$.next([ { name: "bobby's org", id: "1234-3323-23223" }, ] as Organization[]); @@ -196,7 +199,7 @@ describe("VaultPopupListFiltersService", () => { { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -216,7 +219,7 @@ describe("VaultPopupListFiltersService", () => { { name: "catherine's org", id: "77733-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -240,7 +243,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -258,7 +261,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -276,7 +279,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual([ @@ -488,7 +491,7 @@ describe("VaultPopupListFiltersService", () => { state$.next(true); service.filterVisibilityState$.subscribe((filterVisibility) => { - expect(filterVisibility).toBeTrue(); + expect(filterVisibility).toBe(true); done(); }); }); @@ -496,7 +499,7 @@ describe("VaultPopupListFiltersService", () => { it("updates stored filter state", async () => { await service.updateFilterVisibility(false); - expect(update).toHaveBeenCalledOnce(); + expect(update).toHaveBeenCalledTimes(1); // Get callback passed to `update` const updateCallback = update.mock.calls[0][0]; expect(updateCallback()).toBe(false); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 8455fd587d0..6190d14a6a7 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -208,7 +208,9 @@ export class VaultPopupListFiltersService { * Organization array structured to be directly passed to `ChipSelectComponent` */ organizations$: Observable[]> = combineLatest([ - this.organizationService.memberOrganizations$, + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.memberOrganizations$(account?.id)), + ), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ diff --git a/apps/browser/src/vault/popup/services/vault-popup-section.service.ts b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts new file mode 100644 index 00000000000..ed641e0cdf7 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts @@ -0,0 +1,129 @@ +import { computed, effect, inject, Injectable, signal, Signal } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + +import { + KeyDefinition, + StateProvider, + VAULT_SETTINGS_DISK, +} from "@bitwarden/common/platform/state"; + +import { VaultPopupItemsService } from "./vault-popup-items.service"; + +export type PopupSectionOpen = { + favorites: boolean; + allItems: boolean; +}; + +const SECTION_OPEN_KEY = new KeyDefinition(VAULT_SETTINGS_DISK, "sectionOpen", { + deserializer: (obj) => obj, +}); + +const INITIAL_OPEN: PopupSectionOpen = { + favorites: true, + allItems: true, +}; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupSectionService { + private vaultPopupItemsService = inject(VaultPopupItemsService); + private stateProvider = inject(StateProvider); + + private hasFilterOrSearchApplied = toSignal( + this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)), + ); + + /** + * Used to change the open/close state without persisting it to the local disk. Reflects + * application-applied overrides. + * `null` means there is no current override + */ + private temporaryStateOverride = signal | null>(null); + + constructor() { + effect( + () => { + /** + * auto-open all sections when search or filter is applied, and remove + * override when search or filter is removed + */ + if (this.hasFilterOrSearchApplied()) { + this.temporaryStateOverride.set(INITIAL_OPEN); + } else { + this.temporaryStateOverride.set(null); + } + }, + { + allowSignalWrites: true, + }, + ); + } + + /** + * Stored disk state for the open/close state of the sections. Will be `null` if user has never + * opened/closed a section + */ + private sectionOpenStateProvider = this.stateProvider.getGlobal(SECTION_OPEN_KEY); + + /** + * Stored disk state for the open/close state of the sections, with an initial value provided + * if the stored disk state does not yet exist. + */ + private sectionOpenStoredState = toSignal( + this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)), + // Indicates that the state value is loading + { initialValue: null }, + ); + + /** + * Indicates the current open/close display state of each section, accounting for temporary + * non-persisted overrides. + */ + sectionOpenDisplayState: Signal> = computed(() => ({ + ...this.sectionOpenStoredState(), + ...this.temporaryStateOverride(), + })); + + /** + * Retrieve the open/close display state for a given section. + * + * @param sectionKey section key + */ + getOpenDisplayStateForSection(sectionKey: keyof PopupSectionOpen): Signal { + return computed(() => this.sectionOpenDisplayState()?.[sectionKey]); + } + + /** + * Updates the stored open/close state of a given section. Should be called only when a user action + * is taken directly to change the open/close state. + * + * Removes any current temporary override for the given section, as direct user action should + * supersede any application-applied overrides. + * + * @param sectionKey section key + */ + async updateSectionOpenStoredState( + sectionKey: keyof PopupSectionOpen, + open: boolean, + ): Promise { + await this.sectionOpenStateProvider.update((currentState) => { + return { + ...(currentState ?? INITIAL_OPEN), + [sectionKey]: open, + }; + }); + + this.temporaryStateOverride.update((prev) => { + if (prev !== null) { + return { + ...prev, + [sectionKey]: open, + }; + } + + return prev; + }); + } +} diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts deleted file mode 100644 index f50d6ebc236..00000000000 --- a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Injectable } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { - GlobalState, - KeyDefinition, - StateProvider, - VAULT_BROWSER_UI_ONBOARDING, -} from "@bitwarden/common/platform/state"; -import { DialogService } from "@bitwarden/components"; - -import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component"; - -// Key definition for the Vault UI onboarding state. -// This key is used to store the state of the new UI information dialog. -export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition( - VAULT_BROWSER_UI_ONBOARDING, - "dialogState", - { - deserializer: (obj) => obj, - }, -); - -@Injectable() -export class VaultUiOnboardingService { - private onboardingUiReleaseDate = new Date("2024-12-10"); - - private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal( - GLOBAL_VAULT_UI_ONBOARDING, - ); - - private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe( - map((x) => x ?? false), - ); - - constructor( - private stateProvider: StateProvider, - private dialogService: DialogService, - private apiService: ApiService, - ) {} - - /** - * Checks whether the onboarding dialog should be shown and opens it if necessary. - * The dialog is shown if the user has not previously viewed it and is not a new account. - */ - async showOnboardingDialog(): Promise { - const hasViewedDialog = await this.getVaultUiOnboardingState(); - - if (!hasViewedDialog && !(await this.isNewAccount())) { - await this.openVaultUiOnboardingDialog(); - } - } - - private async openVaultUiOnboardingDialog(): Promise { - const dialogRef = VaultUiOnboardingComponent.open(this.dialogService); - - const result = firstValueFrom(dialogRef.closed); - - // Update the onboarding state when the dialog is closed - await this.setVaultUiOnboardingState(true); - - return result; - } - - private async isNewAccount(): Promise { - const userProfile = await this.apiService.getProfile(); - const profileCreationDate = new Date(userProfile.creationDate); - return profileCreationDate > this.onboardingUiReleaseDate; - } - - /** - * Updates and saves the state indicating whether the user has viewed - * the new UI onboarding information dialog. - */ - private async setVaultUiOnboardingState(value: boolean): Promise { - await this.vaultUiOnboardingState.update(() => value); - } - - /** - * Retrieves the current state indicating whether the user has viewed - * the new UI onboarding information dialog.s - */ - private async getVaultUiOnboardingState(): Promise { - return await firstValueFrom(this.vaultUiOnboardingState$); - } -} diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index 7f300a508a6..3aab9f935e4 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -16,8 +16,14 @@ import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SelectModule } from "../../../../../../libs/components/src/select/select.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts index b1db949f2ee..8abc3f906c0 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts @@ -15,8 +15,14 @@ import { } from "@bitwarden/components"; import { VaultIcons } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ItemModule } from "../../../../../../libs/components/src/item/item.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html index a79b6c74b03..c77ac4f9755 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html @@ -13,10 +13,25 @@

[appA11yTitle]="'viewItemTitle' | i18n: cipher.name" (click)="onViewCipher(cipher)" > - +
+ +
{{ cipher.name }} + + + {{ cipher.subTitle }} - + - diff --git a/apps/desktop/src/auth/login/login-v1.component.ts b/apps/desktop/src/auth/login/login-v1.component.ts index 5b1a1c68d29..f78bee7323d 100644 --- a/apps/desktop/src/auth/login/login-v1.component.ts +++ b/apps/desktop/src/auth/login/login-v1.component.ts @@ -11,11 +11,9 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, - RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -77,8 +75,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - webAuthnLoginService: WebAuthnLoginServiceAbstraction, - registerRouteService: RegisterRouteService, toastService: ToastService, private configService: ConfigService, ) { @@ -100,8 +96,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe route, loginEmailService, ssoLoginService, - webAuthnLoginService, - registerRouteService, toastService, ); this.onSuccessfulLogin = () => { @@ -228,7 +222,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe } // Save off email for SSO - await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); + await this.ssoLoginService.setSsoEmail(this.formGroup.controls.email.value); // Generate necessary sso params const passwordOptions: any = { @@ -247,8 +241,11 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe // Save sso params await this.ssoLoginService.setSsoState(state); await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier); + try { await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (err) { this.platformUtilsService.showToast( "error", diff --git a/apps/desktop/src/auth/two-factor-auth-duo.component.ts b/apps/desktop/src/auth/two-factor-auth-duo.component.ts index 72137dc5364..c238b753b64 100644 --- a/apps/desktop/src/auth/two-factor-auth-duo.component.ts +++ b/apps/desktop/src/auth/two-factor-auth-duo.component.ts @@ -21,6 +21,8 @@ import { TypographyModule, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; const BroadcasterSubscriptionId = "TwoFactorComponent"; diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts index 29271b565c1..d8b80c28df1 100644 --- a/apps/desktop/src/auth/two-factor-auth.component.ts +++ b/apps/desktop/src/auth/two-factor-auth.component.ts @@ -4,19 +4,44 @@ import { Component } from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; import { RouterLink } from "@angular/router"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthEmailComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthWebAuthnComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { JslibModule } from "../../../../libs/angular/src/jslib.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CheckboxModule } from "../../../../libs/components/src/checkbox"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../libs/components/src/link"; -import { I18nPipe } from "../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../libs/components/src/typography"; import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; @@ -45,6 +70,5 @@ import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; TwoFactorAuthDuoComponent, TwoFactorAuthWebAuthnComponent, ], - providers: [I18nPipe], }) export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent {} diff --git a/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts index e69ebca3630..9e5755dd579 100644 --- a/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts +++ b/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts @@ -117,15 +117,15 @@ describe("biometrics tests", function () { const testCases = [ // happy path [true, false, false, BiometricsStatus.Available], - [false, true, true, BiometricsStatus.AutoSetupNeeded], - [false, true, false, BiometricsStatus.ManualSetupNeeded], - [false, false, false, BiometricsStatus.HardwareUnavailable], + [false, true, true, BiometricsStatus.HardwareUnavailable], + [true, true, true, BiometricsStatus.AutoSetupNeeded], + [true, true, false, BiometricsStatus.ManualSetupNeeded], // should not happen [false, false, true, BiometricsStatus.HardwareUnavailable], - [true, true, true, BiometricsStatus.Available], - [true, true, false, BiometricsStatus.Available], [true, false, true, BiometricsStatus.Available], + [false, true, false, BiometricsStatus.HardwareUnavailable], + [false, false, false, BiometricsStatus.HardwareUnavailable], ]; for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) { diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts index 06956503a05..d0ba66fdad4 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -60,6 +60,8 @@ export class MainBiometricsService extends DesktopBiometricsService { */ async getBiometricsStatus(): Promise { if (!(await this.osBiometricsService.osSupportsBiometric())) { + return BiometricsStatus.HardwareUnavailable; + } else { if (await this.osBiometricsService.osBiometricsNeedsSetup()) { if (await this.osBiometricsService.osBiometricsCanAutoSetup()) { return BiometricsStatus.AutoSetupNeeded; @@ -67,8 +69,6 @@ export class MainBiometricsService extends DesktopBiometricsService { return BiometricsStatus.ManualSetupNeeded; } } - - return BiometricsStatus.HardwareUnavailable; } return BiometricsStatus.Available; } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 1f569319953..6a5764e4c9f 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Fout" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januarie" }, @@ -871,6 +885,15 @@ "message": "Bevestig met Duo Security vir u organisasie d.m.v. die Duo Mobile-toep, SMS, spraakoproep of ’n U2F-beveiligingsleutel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Swak wagwoord geidentifiseer en gevind in 'n data lekkasie. Gebruik 'n sterk en unieke wagwoord om jou rekening te beskerm. Is jy seker dat jy hierdie wagwoord wil gebruik?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Kontroleer bekende data lekkasies vir hierdie wagwoord" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index a15dbb5f078..ecbacf95a8d 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -249,6 +249,20 @@ "error": { "message": "خطأ" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "يناير" }, @@ -871,6 +885,15 @@ "message": "تحقق من خلال نظام الحماية الثنائي لمؤسستك باستخدام تطبيق Duo Mobile أو الرسائل القصيرة أو المكالمة الهاتفية أو مفتاح الأمان U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "كلمة مرور ضعيفة محددة وموجودة في خرق البيانات. استخدم كلمة مرور قوية وفريدة لحماية حسابك. هل أنت متأكد من أنك تريد استخدام كلمة المرور هذه؟" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "تحقق من خروقات البيانات المعروفة لكلمة المرور هذه" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "لم يُعثر على أي منفذ مجاني لتسجيل الدخول." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "السماح" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "تغيير البريد الإلكتروني الخاص بالحساب" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 07519429f12..dfd7942e033 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Xəta" }, + "decryptionError": { + "message": "Şifrə açma xətası" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden, aşağıda sadalanan seyf element(lər)inin şifrəsini aça bilmədi." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Əlavə data itkisini önləmək üçün", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "müştəri dəstəyi ilə əlaqə saxlayın.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Yanvar" }, @@ -871,6 +885,15 @@ "message": "Təşkilatınızını Duo Security ilə doğrulamaq üçün Duo Mobile tətbiqi, SMS, telefon zəngi və ya U2F güvənlik açarını istifadə edin.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Kimliyinizi doğrulayın" + }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Zəif parol məlumat pozuntusunda aşkarlandı və tapıldı. Hesabınızı qorumaq üçün güclü və unikal bir parol istifadə edin. Bu parolu istifadə etmək istədiyinizə əminsiniz?" }, + "useThisPassword": { + "message": "Bu parolu istifadə et" + }, + "useThisUsername": { + "message": "Bu istifadəçi adını istifadə et" + }, "checkForBreaches": { "message": "Bu parol üçün bilinən məlumat pozuntularını yoxlayın" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "SSO giriş üçün açıq port tapıla bilmədi." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Əvvəlcə PIN və ya parol ilə kilid açma tələb olunduğu üçün biometrik ilə kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrik kilid açma indi əlçatmazdır." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Bitwarden masaüstü tətbiqində $EMAIL$ üçün fəal olmadığına görə biometrik kilid açma əlçatmazdır.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Bilinməyən bilinməyən bir səbəbə görə biometrik kilid açma əlçatmazdır." + }, "authorize": { "message": "Səlahiyyət ver" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Hesabın e-poçtunu dəyişdir" + }, + "organizationUpgradeRequired": { + "message": "Təşkilat yüksəltmə tələb olunur" + }, + "upgradeOrganization": { + "message": "Təşkilatı yüksəlt" + }, + "upgradeOrganizationDesc": { + "message": "Bu özəllik, ödənişsiz təşkilatlar üçün əlçatan deyil. Daha çox özəlliyin kilidini açmaq üçün ödənişli plana keçin." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Uzantını güncəlləmək tələb olunur" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "İstifadə etdiyiniz brauzer uzantısı köhnəlib. Lütfən onu güncəlləyin, ya da masaüstü tətbiq ayarlarında brauzer inteqrasiyası üzrə barmaq izi ilə doğrulamanı sıradan çıxardın." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index e89d6b5fa6f..7b8cc90d072 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Памылка" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Студзень" }, @@ -871,6 +885,15 @@ "message": "Праверка з дапамогай Duo Security для вашай арганізацыі, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Вызначаны ненадзейны пароль, які знойдзены ва ўцечках даных. Выкарыстоўвайце надзейныя і ўнікальныя паролі для абароны свайго ўліковага запісу. Вы сапраўды хочаце выкарыстоўваць гэты пароль?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Праверыць у вядомых уцечках даных для гэтага пароля" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 9770e24c5ad..48dc71a16cd 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Грешка" }, + "decryptionError": { + "message": "Грешка при дешифриране" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Битоурден не може да дешифрира елементите от трезора посочени по-долу." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Свържете се с поддръжката", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "за да избегнете загубата на данни.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "януари" }, @@ -871,6 +885,15 @@ "message": "Удостоверяване чрез Duo Security за организацията ви, с ползване на приложението Duo Mobile, SMS, телефонен разговор или устройство U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Потвърдете самоличността си" + }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Разпозната е слаба парола. Използвайте силна парола, за да защитете данните си. Наистина ли искате да използвате слаба парола?" }, + "useThisPassword": { + "message": "Използване на тази парола" + }, + "useThisUsername": { + "message": "Използване на това потребителско име" + }, "checkForBreaches": { "message": "Проверяване в известните случаи на изтекли данни за тази парола" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Не могат да бъдат открити свободни портове за еднократната идентификация." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Отключването с биометрични данни не е налично, тъй като първо се изисква отключване чрез ПИН или парола." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Отключването с биометрични данни не е налично в момента." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Отключването с биометрични данни не е налично, тъй като не е включено за $EMAIL$ в приложението на Битуорден за компютър.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Отключването с биометрични данни не е налично по неизвестна причина." + }, "authorize": { "message": "Упълномощаване" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Промяна на е-пощата" + }, + "organizationUpgradeRequired": { + "message": "Необходимо е надграждане на организацията" + }, + "upgradeOrganization": { + "message": "Надграждане на организацията" + }, + "upgradeOrganizationDesc": { + "message": "Безплатните планове нямат достъп до тази функционалност. Преминете към платен план, за да се възползвате от тази и много други възможности." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Необходимо е обновяване на добавката" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Добавката за браузъра, която използвате, е остаряла. Моля, обновете я или изключете интеграцията за проверка на пръстов отпечатък в браузъра от настройките на самостоятелното приложение." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 8fe871a956f..e559bf83b22 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -249,6 +249,20 @@ "error": { "message": "ত্রুটি" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "জানুয়ারী" }, @@ -871,6 +885,15 @@ "message": "Duo Mobile app, এসএমএস, ফোন কল, বা U2F সুরক্ষা কী ব্যবহার করে আপনার সংস্থার জন্য Duo Security এর মাধ্যমে যাচাই করুন।", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index e90a7e16c89..b4c616cdd01 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Greška" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Potvrdi sa Duo Security za svoju organizaciju pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index ace7e5defc0..13ba0aebb4e 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -27,7 +27,7 @@ "message": "Nota segura" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "folders": { "message": "Carpetes" @@ -64,7 +64,7 @@ } }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvingut/da de nou" }, "moveToOrgDesc": { "message": "Trieu una organització a la qual vulgueu desplaçar aquest element. El trasllat a una organització transfereix la propietat de l'element a aquesta organització. Ja no sereu el propietari directe d'aquest element una vegada s'haja mogut." @@ -181,16 +181,16 @@ "message": "Adreça" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clau privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clau pública" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Empremta digital" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipus de clau" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -205,22 +205,22 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "S'ha generat una nova clau SSH" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "La contrasenya que heu introduït és incorrecta." }, "importSshKey": { - "message": "Import" + "message": "Importa" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Confirma contrasenya" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Introduïu la contrasenya per a la clau SSH." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "Introduïu la contrasenya" }, "sshAgentUnlockRequired": { "message": "Please unlock your vault to approve the SSH key request." @@ -229,7 +229,7 @@ "message": "SSH key request timed out." }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "Habilita agent SSH" }, "enableSshAgentDesc": { "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Error de desxifrat" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacteu amb el servei d'atenció al client", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "per evitar la pèrdua de dades addicionals.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Gener" }, @@ -323,7 +337,7 @@ "message": "Genera contrasenya" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera frase de pas" }, "type": { "message": "Tipus" @@ -461,13 +475,13 @@ "message": "Copia contrasenya" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "Regenera clau SSH" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "Copia la clau privada SSH" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "Copia frase de pas", "description": "Copy passphrase to clipboard" }, "copyUri": { @@ -498,11 +512,11 @@ "message": "Caràcters especials (!@#$%^&*)" }, "include": { - "message": "Include", + "message": "Inclou", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Inclou majúscules", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -510,7 +524,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Inclou minúscules", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -518,7 +532,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Inclou números", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -526,7 +540,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Inclou caràcters especials", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -561,7 +575,7 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Eviteu caràcters ambigus", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -624,7 +638,7 @@ "message": "Crea un compte" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou a Bitwarden?" }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" @@ -636,16 +650,16 @@ "message": "Inicia sessió" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Inicia sessió a Bitwarden" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Inicieu sessió amb la clau de pas" }, "loginWithDevice": { - "message": "Log in with device" + "message": "Inici de sessió amb dispositiu" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usa inici de sessió únic" }, "submit": { "message": "Envia" @@ -691,10 +705,10 @@ "message": "Pista de la contrasenya mestra" }, "joinOrganization": { - "message": "Uneix-te a l'organització" + "message": "Uniu-vos a l'organització" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Uniu-vos a $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -709,16 +723,16 @@ "message": "Configuració" }, "accountEmail": { - "message": "Account email" + "message": "Correu electrònic del compte" }, "requestHint": { - "message": "Request hint" + "message": "Sol·licita pista" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Sol·licita pista de la contrasenya" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Introduïu l'adreça de correu electrònic del compte i se us enviarà la pista de contrasenya" }, "passwordHint": { "message": "Pista per a la contrasenya" @@ -767,7 +781,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Heu iniciat sessió!" }, "masterPassSent": { "message": "Hem enviat un correu electrònic amb la vostra contrasenya mestra." @@ -871,6 +885,15 @@ "message": "Verifiqueu amb Duo Security per a la vostra organització mitjançant l'aplicació Duo Mobile, SMS, trucada telefònica o clau de seguretat U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifiqueu la vostra identitat" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continua l'inici de sessió" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -881,7 +904,7 @@ "message": "Correu electrònic" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Introduïu el codi que us hem enviat al correu electrònic." }, "loginUnavailable": { "message": "Inici de sessió no disponible" @@ -920,13 +943,13 @@ "message": "URL del servidor" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Temps d'espera d'autenticació" }, "authenticationSessionTimedOut": { "message": "The authentication session timed out. Please restart the login process." }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL del servidor autoallotjat", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -975,7 +998,7 @@ "message": "La vostra sessió ha caducat." }, "restartRegistration": { - "message": "Restart registration" + "message": "Reinicia el registre" }, "expiredLink": { "message": "Enllaç caducat" @@ -984,7 +1007,7 @@ "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "És possible que ja tingueu un compte" }, "logOutConfirmation": { "message": "Segur que voleu tancar la sessió?" @@ -1073,16 +1096,16 @@ "message": "La caixa forta està bloquejada. Comproveu la contrasenya mestra per continuar." }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "El compte està bloquejat" }, "or": { - "message": "or" + "message": "o" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "Desbloqueja amb dades biomètriques" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Desbloqueja amb contrasenya mestra" }, "unlock": { "message": "Desbloqueja" @@ -1113,7 +1136,7 @@ "message": "Temps d'espera de la caixa forta" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Temps d'espera" }, "vaultTimeoutDesc": { "message": "Trieu quan es tancarà la vostra caixa forta i feu l'acció seleccionada." @@ -1320,7 +1343,7 @@ "description": "Copy credit card number" }, "copyEmail": { - "message": "Copy email" + "message": "Copia el correu electrònic" }, "copySecurityCode": { "message": "Copia el codi de seguretat", @@ -1393,13 +1416,13 @@ "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "clear": { "message": "Esborra", @@ -1409,10 +1432,10 @@ "message": "No hi ha cap contrasenya a llistar." }, "clearHistory": { - "message": "Clear history" + "message": "Neteja l'historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Res a mostrar" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1492,7 +1515,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "Còpia correcta" }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" @@ -1597,31 +1620,31 @@ "message": "Format de fitxer" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Aquesta exportació de fitxers estarà protegida amb contrasenya i requerirà la contrasenya del fitxer per desxifrar-la." }, "filePassword": { "message": "Contrasenya del fitxer" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Aquesta contrasenya s'utilitzarà per exportar i importar aquest fitxer" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Utilitzeu la clau de xifratge del vostre compte, derivada del nom d'usuari i la contrasenya mestra, per xifrar l'exportació i restringir la importació només al compte de Bitwarden actual." }, "passwordProtected": { "message": "Protegit amb contrasenya" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Establiu una contrasenya per xifrar l'exportació i importeu-la a qualsevol compte de Bitwarden mitjançant aquesta contrasenya." }, "exportTypeHeading": { "message": "Tipus d'exportació" }, "accountRestricted": { - "message": "Account restricted" + "message": "Compte restringit" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Contrasenya del fitxer\" i \"Confirma contrasenya del fitxer\" no coincideixen." }, "hCaptchaUrl": { "message": "Url hCaptcha", @@ -1747,7 +1770,7 @@ "message": "Sol·liciteu Windows Hello en iniciar" }, "autoPromptPolkit": { - "message": "Ask for system authentication on launch" + "message": "Sol·licita l'autenticació del sistema a l'inici" }, "autoPromptTouchId": { "message": "Sol·liciteu Touch ID en iniciar" @@ -1759,7 +1782,7 @@ "message": "Recomanat per seguretat." }, "lockWithMasterPassOnRestart1": { - "message": "Lock with master password on restart" + "message": "Bloqueja amb la contrasenya mestra en reiniciar" }, "deleteAccount": { "message": "Suprimeix el compte" @@ -1771,7 +1794,7 @@ "message": "La supressió del compte és permanent. No es pot desfer." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "No es pot suprimir el compte" }, "cannotDeleteAccountDesc": { "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." @@ -1887,7 +1910,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": "de $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -1960,7 +1983,7 @@ "message": "en qualsevol moment." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "En continuar, acceptes el" }, "and": { "message": "i" @@ -2234,7 +2257,7 @@ "message": "Es requereix verificació per correu electrònic" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Correu electrònic verificat" }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el vostre correu electrònic per utilitzar aquesta característica." @@ -2348,7 +2371,7 @@ "message": "El temps d'espera de la caixa forta supera les restriccions establertes per la vostra organització." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Invitació acceptada" }, "resetPasswordPolicyAutoEnroll": { "message": "Inscripció automàtica" @@ -2420,7 +2443,7 @@ "message": "Canvia de compte" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Ja teniu un compte?" }, "options": { "message": "Opcions" @@ -2441,10 +2464,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "S'està exportant la caixa forta de l’organització" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Només s'exportarà la caixa forta de l'organització associada a $ORGANIZATION$. No s'inclouran els elements de les caixes fortes individuals ni d'altres organitzacions.", "placeholders": { "organization": { "content": "$1", @@ -2456,7 +2479,7 @@ "message": "Bloquejat" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "La caixa forta està bloquejada" }, "unlocked": { "message": "Desbloquejat" @@ -2478,10 +2501,10 @@ "message": "Genera un nom d'usuari" }, "generateEmail": { - "message": "Generate email" + "message": "Genera correu electrònic" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "El valor ha d'estar entre $MIN$ i $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2495,7 +2518,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Utilitzeu $RECOMMENDED$ caràcters o més per generar una contrasenya segura.", "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 +2528,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Utilitzeu $RECOMMENDED$ paraules o més per generar una frase de pas segura.", "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,7 +2581,7 @@ "message": "Genera un àlies de correu electrònic amb un servei de reenviament extern." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Domini del correu electrònic", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -2566,7 +2589,7 @@ "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Error de $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2580,11 +2603,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generat per Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Lloc web: $WEBSITE$. Generat per Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2726,13 +2749,13 @@ "message": "S'ha enviat una notificació al vostre dispositiu." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "S'ha enviat una notificació al vostre dispositiu" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Assegureu-vos que la vostra caixa forta estiga desbloquejada i que la frase d'empremta digital coincidisca en l'altre dispositiu" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Necessiteu una altra opció?" }, "fingerprintMatchInfo": { "message": "Assegureu-vos que la vostra caixa forta estiga desbloquejada i que la frase d'empremta digital coincidisca amb l'altre dispositiu." @@ -2747,7 +2770,7 @@ "message": "L'inici de sessió amb el dispositiu ha d'estar activat a la configuració de l'aplicació Bitwarden. Necessiteu una altra opció?" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "viewAllLoginOptions": { "message": "Veure totes les opcions d'inici de sessió" @@ -2845,7 +2868,7 @@ "message": "and continue creating your account." }, "noEmail": { - "message": "No email?" + "message": "Sense correu electrònic?" }, "goBack": { "message": "Torna arrere" @@ -2865,17 +2888,23 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Contrasenya feble identificada i trobada en una filtració de dades. Utilitzeu una contrasenya única i segura per protegir el vostre compte. Esteu segur que voleu utilitzar aquesta contrasenya?" }, + "useThisPassword": { + "message": "Utilitzeu aquesta contrasenya" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Comproveu les filtracions de dades conegudes per a aquesta contrasenya" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Connectat!" }, "important": { "message": "Important:" }, "accessing": { - "message": "Accessing" + "message": "Accedint a" }, "accessTokenUnableToBeDecrypted": { "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." @@ -3354,19 +3383,43 @@ "message": "Dades" }, "fileSends": { - "message": "File Sends" + "message": "Sends de fitxers" }, "textSends": { - "message": "Text Sends" + "message": "Sends de text" }, "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { - "message": "Authorize" + "message": "Autoritza" }, "deny": { - "message": "Deny" + "message": "Denega" }, "sshkeyApprovalTitle": { "message": "Confirm SSH key usage" @@ -3375,7 +3428,7 @@ "message": "is requesting access to" }, "unknownApplication": { - "message": "An application" + "message": "Una aplicació" }, "sshKeyPasswordUnsupported": { "message": "Importing password protected SSH keys is not yet supported" @@ -3396,10 +3449,10 @@ "message": "File saved to device. Manage from your device downloads." }, "importantNotice": { - "message": "Important notice" + "message": "Noticia important" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Configura inici de sessió en dos passos" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -3408,7 +3461,7 @@ "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." }, "remindMeLater": { - "message": "Remind me later" + "message": "Recorda-m'ho més tard" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -3420,15 +3473,30 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "No, jo no" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { "message": "Yes, I can reliably access my email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Activa l'inici de sessió en dos passos" }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Actualitza l'organització" + }, + "upgradeOrganizationDesc": { + "message": "Aquesta característica no està disponible per a organitzacions gratuïtes. Canvieu a un pla de pagament per desbloquejar més característiques." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Cal actualitzar l'extensió" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index f73f76f44a5..a4f1f50ce93 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Chyba" }, + "decryptionError": { + "message": "Chyba dešifrování" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nemohl dešifrovat níže uvedené položky v trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznickou podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "abyste zabránili ztrátě dat.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Leden" }, @@ -871,6 +885,15 @@ "message": "Ověření pomocí Duo Security pro Vaši organizaci prostřednictvím aplikace Duo Mobile, SMS, telefonního hovoru nebo bezpečnostního klíče U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Ověřte svou totožnost" + }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Slabé heslo bylo nalezeno mezi odhalenými hesly. K zabezpečení Vašeho účtu používejte silné a jedinečné heslo. Opravdu chcete používat toto heslo?" }, + "useThisPassword": { + "message": "Použít toto heslo" + }, + "useThisUsername": { + "message": "Použít toto uživatelské jméno" + }, "checkForBreaches": { "message": "Zkontrolovat heslo, zda nebylo odhaleno" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Pro přihlášení SSO nebyly nalezeny žádné volné porty." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrické odemknutí je nedostupné, protože je potřeba nejprve odemknout pomocí PIN nebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrické odemknutí je momentálně nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometrické odemknutí není k dispozici, protože není povoleno pro $EMAIL$ v desktopové aplikaci Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrické odemknutí je momentálně z neznámého důvodu nedostupné." + }, "authorize": { "message": "Autorizovat" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Změnit e-mail účtu" + }, + "organizationUpgradeRequired": { + "message": "Je vyžadována aktualizace organizace" + }, + "upgradeOrganization": { + "message": "Aktualizovat organizaci" + }, + "upgradeOrganizationDesc": { + "message": "Tato funkce není dostupná pro bezplatné organizace. Přepněte na placenou verzi a odemkněte další funkce." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Je vyžadována aktualiazce rozšíření" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Použité rozšíření prohlížeče je zastaralé. Aktualizujte jej nebo zakažte ověření otisků prstů při integraci prohlížeče v nastavení aplikace." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 00d79c80ec8..22e19c84310 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index b2867c4072e..5545e856b64 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Fejl" }, + "decryptionError": { + "message": "Dekrypteringsfejl" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kunne ikke dekryptere boks-emne(r) anført nedenfor." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontakt kundeservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "for at undgå yderligere tab af data.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Bekræft med Duo Security for din organisation vha. Duo Mobile-app, SMS, telefonopkald eller U2F-sikkerhedsnøgle.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Bekræft identiteten" + }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Svag adgangskode identificeret og fundet i datalæk. Brug en unik adgangskode til at beskytte din konto. Sikker på, at at denne adgangskode skal bruges?" }, + "useThisPassword": { + "message": "Anvend denne adgangskode" + }, + "useThisUsername": { + "message": "Anvend dette brugernavn" + }, "checkForBreaches": { "message": "Tjek kendte datalæk for denne adgangskode" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Ingen ledige porte fundet til SSO-login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisk oplåsning er utilgængelig, da PIN- eller adgangskode kræves først." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisk oplåsning er p.t. utilgængelig." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometrisk oplåsning er utilgængelig, da det ikke er aktiveret for $EMAIL$ i Bitwarden computer-appen.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisk oplåsning er p.t. utilgængelig grundet en ukendt årsag." + }, "authorize": { "message": "Godkend" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Skift kontoe-mailadresse" + }, + "organizationUpgradeRequired": { + "message": "Organisationsopgradering krævet" + }, + "upgradeOrganization": { + "message": "Opgradér organisation" + }, + "upgradeOrganizationDesc": { + "message": "Denne funktion er utilgængelig for gratis organisationer. Skift til en betalingsabonnementstype for at oplåse flere funktioner." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Udvidelsesopdatering krævet" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Den anvendte webbrowserudvidelse er forældet. Opdatér den venligst eller deaktivér webbrowserintegreret fingeraftryksbekræftelse i computer-app indstillingerne." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index ae20bc0931d..4f7740f46c8 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Fehler" }, + "decryptionError": { + "message": "Entschlüsselungsfehler" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktiere unser Customer Success Team", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": ", um zusätzlichen Datenverlust zu vermeiden.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Nutze Duo Security, um dich mit der Duo-Mobile-App, SMS, per Anruf oder U2F-Sicherheitsschlüssel bei deiner Organisation zu verifizieren.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifiziere deine Identität" + }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Schwaches Passwort erkannt und in einem Datendiebstahl gefunden. Verwende ein starkes und einzigartiges Passwort, um dein Konto zu schützen. Bist du sicher, dass du dieses Passwort verwenden möchtest?" }, + "useThisPassword": { + "message": "Dieses Passwort verwenden" + }, + "useThisUsername": { + "message": "Diesen Benutzernamen verwenden" + }, "checkForBreaches": { "message": "Bekannte Datendiebstähle auf dieses Passwort überprüfen" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Es konnten keine freien Ports für die SSO-Anmeldung gefunden werden." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da zuerst mit PIN oder Passwort entsperrt werden muss." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisches Entsperren ist derzeit nicht verfügbar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da es für $EMAIL$ in der Bitwarden Desktop-App nicht aktiviert ist.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisches Entsperren ist derzeit aus einem unbekannten Grund nicht verfügbar." + }, "authorize": { "message": "Autorisieren" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "E-Mail-Adresse des Kontos ändern" + }, + "organizationUpgradeRequired": { + "message": "Organisations-Upgrade erforderlich" + }, + "upgradeOrganization": { + "message": "Organisation upgraden" + }, + "upgradeOrganizationDesc": { + "message": "Diese Funktion ist für kostenlose Organisationen nicht verfügbar. Wechsle zu einem kostenpflichtigen Abonnement, um weitere Funktionen freizuschalten." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Aktualisierung der Erweiterung notwendig" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Die von dir verwendete Browser-Erweiterung ist veraltet. Bitte aktualisiere sie oder deaktiviere die Fingerabdrucküberprüfung der Browser-Integration in den Einstellungen der Desktop-App." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 676c016c699..8f62ceda6ca 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -27,7 +27,7 @@ "message": "Ασφαλής σημείωση" }, "typeSshKey": { - "message": "SSH key" + "message": "Κλειδί SSH" }, "folders": { "message": "Φάκελοι" @@ -64,7 +64,7 @@ } }, "welcomeBack": { - "message": "Welcome back" + "message": "Καλωσορίσατε και πάλι" }, "moveToOrgDesc": { "message": "Επιλέξτε έναν οργανισμό στον οποίο θέλετε να μετακινήσετε αυτό το στοιχείο. Η μετακίνηση σε έναν οργανισμό μεταβιβάζει την ιδιοκτησία του στοιχείου σε αυτό τον οργανισμό. Δεν θα είστε πλέον ο άμεσος ιδιοκτήτης αυτού του στοιχείου μόλις το μετακινήσετε." @@ -107,7 +107,7 @@ "message": "Επεξεργασία στοιχείου" }, "emailAddress": { - "message": "Διεύθυνση ηλ. ταχυδρομείου" + "message": "Διεύθυνση Email" }, "verificationCodeTotp": { "message": "Κωδικός Επαλήθευσης (TOTP)" @@ -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": "Ενεργοποιήστε τον πράκτορα SSH για την υπογραφή αιτημάτων SSH απευθείας από την κρύπτη 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": "Ο πράκτορα SSH είναι μια υπηρεσία που απευθύνεται σε προγραμματιστές που σας επιτρέπει να υπογράψετε αιτήματα SSH απευθείας από την κρύπτη Bitwarden σας." }, "premiumRequired": { "message": "Απαιτείται Premium" @@ -249,6 +249,20 @@ "error": { "message": "Σφάλμα" }, + "decryptionError": { + "message": "Σφάλμα αποκρυπτογράφησης" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Το Bitwarden δεν μπόρεσε να αποκρυπτογραφήσει τα αντικείμενα κρύπτης που αναφέρονται παρακάτω." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Εποικινωνία επιτυχίας επαφής πελάτη", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "για την αποφυγή επιπλέον απώλειας δεδομένων.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Ιανουάριος" }, @@ -461,10 +475,10 @@ "message": "Αντιγραφή κωδικού πρόσβασης" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "Επαναδημιουργία κλειδιού SSH" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "Αντιγραφή ιδιωτικού κλειδιού SSH" }, "copyPassphrase": { "message": "Αντιγραφή φράσης πρόσβασης", @@ -624,7 +638,7 @@ "message": "Δημιουργία λογαριασμού" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Μόλις ήρθες στο Bitwarden;" }, "setAStrongPassword": { "message": "Ορίστε έναν ισχυρό κωδικό πρόσβασης" @@ -636,16 +650,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": "Χρήση single sign-on" }, "submit": { "message": "Υποβολή" @@ -871,6 +885,15 @@ "message": "Επαληθεύστε με το Duo Security για τον οργανισμό σας χρησιμοποιώντας την εφαρμογή Duo Mobile, μήνυμα SMS, τηλεφωνική κλήση ή κλειδί ασφαλείας U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -920,10 +943,10 @@ "message": "URL Διακομιστή" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Χρονικό όριο επαλήθευσης" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Λήξη χρονικού ορίου συνεδρίας επαλήθευσης. Παρακαλώ επανεκκινήστε τη διαδικασία σύνδεσης." }, "selfHostBaseUrl": { "message": "URL διακομιστή αυτο-φιλοξενίας", @@ -1393,13 +1416,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 +1432,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 +1794,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": "Ο λογαριασμός διαγράφηκε" @@ -2481,7 +2504,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 +2518,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 +2528,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 +2749,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 +2764,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": "Δείτε όλες τις επιλογές σύνδεσης" @@ -2865,11 +2888,17 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Βρέθηκε και ταυτοποιήθηκε αδύναμος κωδικός σε μια διαρροή δεδομένων. Χρησιμοποιήστε ένα ισχυρό και μοναδικό κωδικό πρόσβασης για την προστασία του λογαριασμού σας. Είστε σίγουροι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κωδικό πρόσβασης;" }, + "useThisPassword": { + "message": "Χρήση αυτού του κωδικού πρόσβασης" + }, + "useThisUsername": { + "message": "Χρήση αυτού του ονόματος χρήστη" + }, "checkForBreaches": { "message": "Ελέγξτε γνωστές διαρροές δεδομένων για αυτόν τον κωδικό" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Έχετε συνδεθεί!" }, "important": { "message": "Σημαντικό:" @@ -2902,16 +2931,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 +2995,7 @@ "message": "Η διεύθυνση ηλ. ταχυδρομείου του χρήστη λείπει" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Το email χρήστη δεν βρέθηκε. Αποσυνδεθήκατε." }, "deviceTrusted": { "message": "Αξιόπιστη συσκευή" @@ -3362,56 +3391,80 @@ "ssoError": { "message": "Δεν βρέθηκαν ελεύθερες θύρες για τη σύνδεση sso." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή απαιτείται πρώτα το ξεκλείδωμα με PIN ή κωδικό πρόσβασης." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο προς το παρόν." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή δεν είναι ενεργοποιημένο για το $EMAIL$ στην εφαρμογή Bitwarden για υπολογιστές.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο για άγνωστο λόγο." + }, "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": "Ρύθμιση σύνδεσης δύο βημάτων" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Το Bitwarden θα στείλει έναν κωδικό στο email του λογαριασμού σας για να επαληθεύσει τις συνδέσεις από νέες συσκευές ξεκινώντας από τον Φεβρουάριο του 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": "Μπορείτε να ορίσετε σύνδεση δύο βημάτων ως εναλλακτικό τρόπο προστασίας του λογαριασμού σας ή να αλλάξετε το email σας σε ένα που μπορείτε να έχετε πρόσβαση." }, "remindMeLater": { - "message": "Remind me later" + "message": "Υπενθύμιση αργότερα" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Έχετε αξιόπιστη πρόσβαση στο email σας, $EMAIL$;", "placeholders": { "email": { "content": "$1", @@ -3420,15 +3473,30 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Όχι, δεν έχω" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Ναι, μπορώ να συνδεθώ αξιόπιστα στο ηλ. ταχυδρομείο email μου" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Ενεργοποίηση σύνδεσης δύο βημάτων" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Αλλαγή email λογαριασμού" + }, + "organizationUpgradeRequired": { + "message": "Απαιτείται αναβάθμιση οργανισμού" + }, + "upgradeOrganization": { + "message": "Αναβάθμιση οργανισμού" + }, + "upgradeOrganizationDesc": { + "message": "Αυτή η λειτουργία, δεν είναι διαθέσιμη στους δωρεάν οργανισμούς. Μεταβείτε σε ένα πακέτο επί πληρωμής για να ξεκλειδώσετε περισσότερες λειτουργίες." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Απαιτείται ενημέρωση επέκτασης" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Η επέκταση περιηγητή που χρησιμοποιείτε είναι ξεπερασμένη. Παρακαλούμε ενημερώστε την ή απενεργοποιήστε την επικύρωση δακτυλικών αποτυπωμάτων του προγράμματος περιήγησης στις ρυθμίσεις της εφαρμογής για την επιφάνεια εργασίας." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 9ab15230604..4f18a88d994 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3454,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index ee1be4df2e6..986a6ad2e3a 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organisation using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorise" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organisation upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organisation" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organisations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index b8afb36f3a6..9afd76f697a 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organisation using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorise" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organisation upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organisation" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organisations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index b7fbb2fbe93..ce5be3b8c63 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Eraro" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "januaro" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 9cb82ceb92d..df6dded0477 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Enero" }, @@ -871,6 +885,15 @@ "message": "Verificar con Duo Security para tu organización usando la aplicación Duo Mobile, SMS, llamada telefónica o llave de seguridad U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Contraseña débil encontrada en una filtración de datos. Utilice una contraseña única para proteger su cuenta. ¿Está seguro de que desea utilizar una contraseña comprometida?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Comprobar filtración de datos conocidos para esta contraseña" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 276d3565f1a..e7c33c432ba 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Viga" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Jaanuar" }, @@ -871,6 +885,15 @@ "message": "Kinnita organisatsiooni jaoks Duo Security abil, kasutades selleks Duo Mobile rakendust, SMS-i, telefonikõnet või U2F turvavõtit.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Tuvastati nõrk ning andmelekkes lekkinud ülemparool. Kasuta konto paremaks turvamiseks tugevamat parooli. Oled kindel, et soovid nõrga parooliga jätkata?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Otsi seda parooli teadaolevatest andmeleketest" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "SSO-ga sisselogimiseks ei leitud ühtegi vaba porti." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 41e8eda96c7..d0e62383468 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Akatsa" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Urtarrila" }, @@ -871,6 +885,15 @@ "message": "Egiaztatu zure erakunderako Duo Securityrekin Duo Mobile aplikazioa, SMS, telefono deia edo U2F segurtasun-gakoa erabiliz.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index fdc9ba6d31b..ea25d5057e9 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -249,6 +249,20 @@ "error": { "message": "خطا" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "ژانویه" }, @@ -871,6 +885,15 @@ "message": "از Duo Security با استفاده از برنامه تلفن همراه، پیامک، تماس تلفنی یا کلید امنیتی U2F برای تأیید سازمان خود استفاده کنید.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "کلمه عبور ضعیف شناسایی و در یک نقض داده پیدا شد. از یک کلمه عبور قوی و منحصر به فرد برای محافظت از حساب خود استفاده کنید. آیا مطمئنید که می‌خواهید از این کلمه عبور استفاده کنید؟" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "نقض اطلاعات شناخته شده برای این کلمه عبور را بررسی کنید" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 69092054f28..d05cf0e156c 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Virhe" }, + "decryptionError": { + "message": "Salauksen purkuvirhe" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden ei voinut purkaa alla olevia holvin kohteita." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Tammikuu" }, @@ -871,6 +885,15 @@ "message": "Vahvista organisaatiollesi Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-suojausavainta.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Havaittiin heikko ja tietovuodosta löytynyt salasana. Sinun tulisi suojata tilisi vahvalla ja ainutlaatuisella salasanalla. Haluatko varmasti käyttää tätä salasanaa?" }, + "useThisPassword": { + "message": "Käytä tätä salasanaa" + }, + "useThisUsername": { + "message": "Käytä tätä käyttäjätunnusta" + }, "checkForBreaches": { "message": "Tarkasta esiintyykö salasanaa tunnetuissa tietovuodoissa" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Kertakirjautumiselle ei löytynyt vapaita portteja." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrinen lukituksen avaus ei ole tällä hetkellä käytettävissä." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Valtuuta" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Muuta tilin sähköpostiosoitetta" + }, + "organizationUpgradeRequired": { + "message": "Organisaation päivitys vaaditaan" + }, + "upgradeOrganization": { + "message": "Päivitä organisaatio" + }, + "upgradeOrganizationDesc": { + "message": "Ominaisuus ei ole ilmaisorganisaatioiden käytettävissä. Avaa lisää ominaisuuksia vaihtamalla maksulliseen tilaukseen." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index a8a36cd94af..684c57b3c84 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Mali" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Enero" }, @@ -871,6 +885,15 @@ "message": "Patunayan sa Duo Security para sa iyong samahan gamit ang Duo Mobile app, SMS, tawag sa telepono, o U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Mahinang password na nakilala at nakita sa data breach. Gamitin ang malakas at natatanging password upang makaproteksyon sa iyong account. Sigurado ka ba na gusto mong gamitin ang password na ito?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Tingnan ang kilalang breaches ng data para sa password na ito" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index a353653e1e3..476799e97d1 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Erreur" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Janvier" }, @@ -871,6 +885,15 @@ "message": "Sécurisez votre organisation avec Duo Security à l'aide de l'application Duo Mobile, l'envoi d'un SMS, un appel vocal ou une clé de sécurité U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "WebAuthn FIDO2" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Mot de passe faible identifié et trouvé dans une brèche de données. Utilisez un mot de passe robuste et unique pour protéger votre compte. Êtes-vous sûr de vouloir utiliser ce mot de passe ?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Vérifier les brèches de données connues pour ce mot de passe" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Autoriser" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Changer le courriel du compte" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 323d0cd3f7b..4f18a88d994 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index dfaa195895e..a8c34bd61e7 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -249,6 +249,20 @@ "error": { "message": "שגיאה" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "ינואר" }, @@ -871,6 +885,15 @@ "message": "בצע אימות מול Duo Security עבור הארגון שלך באמצעות אפליקצית Duo לפלאפון, SMS, שיחת טלפון, או מפתח אבטחה U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 13bb5edfa93..4a608e89d54 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index a87261a2027..2eceeddde07 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Pogreška" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "siječanj" }, @@ -871,6 +885,15 @@ "message": "Potvrdi s Duo Security za svoju organizaciju pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Slaba lozinka je nađena među ukradenima tijekom krađa podataka. Za zaštitu svog računa koristi jaku i jedinstvenu lozinku. Želiš li svejedno korisiti slabu, ukradenu lozinku?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Provjeri je li lozinka ukradena prilikom krađe podataka" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Nisu nađeni slobodni portovi za SSO prijavu." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Autoriziraj" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Promjeni e-poštu računa" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 11091cde860..809f56c4892 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Hiba" }, + "decryptionError": { + "message": "Visszafejtési hiba" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "A Bitwarden nem tudta visszafejteni az alább felsorolt ​​széf elemeket." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Ügyfélszolgálat elérés siker", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "további adatvesztés elkerülése érdekében.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "január" }, @@ -871,6 +885,15 @@ "message": "Ellenőrzés szervezeti Duo Security segítségével a Duo Mobile alkalmazás, SMS, telefonhívás vagy U2F biztonsági kulcs használatával.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Személyazonosság ellenőrzése" + }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Gyenge jelszó lett azonosítva és megtalálva egy adatvédelmi incidens során. A fók védelme érdekében használjunk erős és egyedi jelszót. Biztosan használni szeretnénk ezt a jelszót?" }, + "useThisPassword": { + "message": "Jelszó használata" + }, + "useThisUsername": { + "message": "Felhasználónév használata" + }, "checkForBreaches": { "message": "Az ehhez a jelszóhoz tartozó ismert adatvédelmi incidensek ellenőrzése" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Nem található szabad port az sso bejelentkezéshez." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "A biometrikus feloldás nem érhető el, mert először PIN kóddal vagy jelszóval kell feloldani." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "A biometrikus feloldás jelenleg nem érhető el." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "A biometrikus feloldás nem érhető el, mert nincs engedélyezve $EMAIL$ számára a Bitwarden asztali alkalmazásban.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "A biometrikus feloldás jelenleg ismeretlen okból nem érhető el." + }, "authorize": { "message": "Hitelesítés" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Fiók email cím megváltoztatása" + }, + "organizationUpgradeRequired": { + "message": "A szervezet felminősítése szükséges." + }, + "upgradeOrganization": { + "message": "Szervezeti áttérés" + }, + "upgradeOrganizationDesc": { + "message": "Ez a szolgáltatás nem elérhető ingyenes szervezeteknek. Váltás fizetős díjcsomagra a további funkciók feloldásához." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Kiterjesztés frissítés szükséges" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Al használt böngésző bővítmény elavult. Frissítsük vagy tiltsuk le a böngésző integráció ujjlenyomat ellenőrzését az asztali alkalmazás beállításainál." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index b9af1c00045..4e5abd62f75 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Galat" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januari" }, @@ -871,6 +885,15 @@ "message": "Verifikasi dengan Duo Security untuk organisasi anda dengan menggunakan Aplikasi Duo Mobile, SMS, panggilan telepon atau kunci keamanan U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Periksa pelanggaran data yang diketahui untuk kata sandi ini" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index e035a13606d..ee7da5c3582 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Errore" }, + "decryptionError": { + "message": "Errore di decifrazione" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden non può decifrare gli elementi elencati di seguito." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contatta il cliente correttamente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "per evitare ulteriori perdite di dati.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Gennaio" }, @@ -871,6 +885,15 @@ "message": "Verifica con Duo Security per la tua organizzazione usando l'app Duo Mobile, SMS, chiamata telefonica, o chiave di sicurezza U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifica la tua identità" + }, + "weDontRecognizeThisDevice": { + "message": "Non riconosciamo questo dispositivo. Inserisci il codice inviato alla tua e-mail per verificare la tua identità." + }, + "continueLoggingIn": { + "message": "Continua l'accesso" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -1320,7 +1343,7 @@ "description": "Copy credit card number" }, "copyEmail": { - "message": "Copia e-mail" + "message": "Copia email" }, "copySecurityCode": { "message": "Copia codice di sicurezza", @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Password debole e trovata in una violazione dei dati. Usa una password forte e unica per proteggere il tuo account. Sei sicuro di voler usare questa password?" }, + "useThisPassword": { + "message": "Usa questa parola d'accesso" + }, + "useThisUsername": { + "message": "Usa questo nome utente" + }, "checkForBreaches": { "message": "Controlla se la tua password è presente in una violazione dei dati" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Non è stato possibile trovare nessuna porta libera per il login Sso." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Lo sblocco biometrico non è disponibile perché è necessario prima sbloccare con PIN o parola d'accesso." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Lo sblocco biometrico non è attualmente disponibile." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Lo sblocco biometrico non è disponibile perché non è abilitato per $EMAIL$ nell'app desktop Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Lo sblocco biometrico non è attualmente disponibile per un motivo sconosciuto." + }, "authorize": { "message": "Autorizza" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Cambia l'e-mail dell'account" + }, + "organizationUpgradeRequired": { + "message": "Aggiornamento dell'organizzazione richiesto" + }, + "upgradeOrganization": { + "message": "Aggiorna organizzazione" + }, + "upgradeOrganizationDesc": { + "message": "Questa funzione non è disponibile per le organizzazioni gratuite. Passa a un piano a pagamento per sbloccare più funzioni." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Aggiornamento estensione richiesto" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "L'estensione del browser che stai usando non è aggiornata. Aggiornala o disabilita la convalida dell'impronta digitale per l'integrazione del browser nelle impostazioni dell'app desktop." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 7315d74a70f..6f6dfaee2f6 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -249,6 +249,20 @@ "error": { "message": "エラー" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "1月" }, @@ -871,6 +885,15 @@ "message": "組織の Duo Security を Duo Mobile アプリや SMS、電話、U2F セキュリティーキーを使用して認証します。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "入力されたパスワードは脆弱かつすでに流出済みです。アカウントを守るためより強力で一意なパスワードを使用してください。本当にこの脆弱なパスワードを使用しますか?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "このパスワードの既知のデータ流出を確認" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "SSO ログインのための空きポートが見つかりませんでした。" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "認可" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "アカウントのメールアドレスを変更する" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 6c554a82700..e7bb32055b7 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -249,6 +249,20 @@ "error": { "message": "შეცდომა" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "იანვარი" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 323d0cd3f7b..4f18a88d994 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 6182bd3a33d..23f4cd3464e 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -249,6 +249,20 @@ "error": { "message": "ದೋಷ" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "ಜನವರಿ" }, @@ -871,6 +885,15 @@ "message": "ಡ್ಯುಯೊ ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಯು 2 ಎಫ್ ಭದ್ರತಾ ಕೀಲಿಯನ್ನು ಬಳಸಿಕೊಂಡು ನಿಮ್ಮ ಸಂಸ್ಥೆಗಾಗಿ ಡ್ಯುಯೊ ಸೆಕ್ಯುರಿಟಿಯೊಂದಿಗೆ ಪರಿಶೀಲಿಸಿ.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 89c5430a5fb..82b41979c09 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -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,31 +205,31 @@ "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." @@ -249,6 +249,20 @@ "error": { "message": "오류" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "1월" }, @@ -461,10 +475,10 @@ "message": "비밀번호 복사" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "SSH 키 재생성" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "SSH 비공개 키 복사" }, "copyPassphrase": { "message": "Copy passphrase", @@ -498,11 +512,11 @@ "message": "특수 문자 (!@#$%^&*)" }, "include": { - "message": "Include", + "message": "포함", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "대문자 포함", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -510,7 +524,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "소문자 포함", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -518,7 +532,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "숫자 포함", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -526,7 +540,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "특수 문자 포함", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -565,7 +579,7 @@ "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "기업 정책에 따른 요구사항이 생성기 옵션에 적용되어 있습니다.", "description": "Indicates that a policy limits the credential generator screen." }, "searchCollection": { @@ -603,7 +617,7 @@ "message": "최대 파일 크기는 500MB입니다." }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "암호화 키 마이그레이션이 필요합니다. 웹 보관함에 로그인하여 암호화 키를 업데이트하세요." }, "editedFolder": { "message": "폴더 편집함" @@ -624,28 +638,28 @@ "message": "계정 만들기" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Bitwarden이 처음 이신가요?" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "강력한 비밀번호 설정" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "비밀번호를 설정하여 계정 생성을 완료하세요" }, "logIn": { "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": "Single sign-on(SSO) 사용" }, "submit": { "message": "보내기" @@ -679,7 +693,7 @@ } }, "masterPassword": { - "message": "Master password" + "message": "마스터 비밀번호" }, "masterPassImportant": { "message": "Your master password cannot be recovered if you forget it!" @@ -871,6 +885,15 @@ "message": "Duo Mobile 앱, SMS, 전화 통화를 사용한 조직용 Duo Security 또는 U2F 보안 키를 사용하여 인증하세요.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 6e07f4ceee3..708393624fc 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Klaida" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Sausis" }, @@ -871,6 +885,15 @@ "message": "Patikrinkite su Duo Security savo organizacijai naudodami Duo Mobile programą, SMS žinutę, telefono skambutį arba U2F saugumo raktą.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Silpnas slaptažodis nustatytas ir rastas per duomenų pažeidimą. Norėdami apsaugoti paskyrą, naudokite stiprų ir unikalų slaptažodį. Ar tikrai norite naudoti šį slaptažodį?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Patikrinti žinomus šio slaptažodžio duomenų pažeidimus" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index a3db76ac646..e26669f30ab 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Kļūda" }, + "decryptionError": { + "message": "Atšifrēšanas kļūda" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nevarēja atšifrēt zemāk uzskaitītos glabātavas vienumus." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Sazināties ar klientu atbalstu", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "lai izvairītos no papildu datu zaudējumiem.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Janvāris" }, @@ -871,6 +885,15 @@ "message": "Apliecināšana ar savas apvienības Duo Security, izmantojot Duo Mobile lietotni, īsziņu, tālruņa zvanu vai U2F drošības atslēgu.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Apliecināt savu identitāti" + }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Noteikta vāja parole, un tā ir atrasta datu noplūdē. Jāizmanto spēcīga un neatkārtojama parole, lai aizsargātu savu kontu. Vai tiešām izmantot šo paroli?" }, + "useThisPassword": { + "message": "Izmantot šo paroli" + }, + "useThisUsername": { + "message": "Izmantot šo lietotājvārdu" + }, "checkForBreaches": { "message": "Meklēt šo paroli zināmās datu noplūdēs" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Netika atrasti brīvi vienotās (SSO) pieteikšanās porti." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama, jo vispirms ir nepieciešama atslēgšana ar PIN vai paroli." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Atslēgšana ar biometriju nav pieejama, jo tā nav iespējota $EMAIL$ Bitwarden darbvirsmas lietotnē.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama nezināma iemesla dēļ." + }, "authorize": { "message": "Pilnvarot" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Mainīt konta e-pasta adresi" + }, + "organizationUpgradeRequired": { + "message": "Nepieciešams apvienības uzlabojums" + }, + "upgradeOrganization": { + "message": "Uzlabot apvienību" + }, + "upgradeOrganizationDesc": { + "message": "Šī iespēja nav pieejama bezmaksas apvienībām. Maksas plāna izvēle sniedz plašākas iespējas." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Nepieciešama paplašinājuma atjaunināšana" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Izmantotais pārlūka paplašinājums ir novecojis. Lūgums atjaunināt to vai atspējot pārlūka sasaistīšanas pirkstu nospieduma pārbaudi darbvirsmas lietotnes iestatījumos." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 49bf2c60f6c..46d8cfe8cb7 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Greška" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Potvrdite sa Duo Security za svoju organizaciju pomoću aplikacije Duo Mobile, SMS-a, telefonskog poziva ili U2F sigurnosnog ključa.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index d314ee44578..1d27bc2182c 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -249,6 +249,20 @@ "error": { "message": "പിശക്" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "ജനുവരി" }, @@ -871,6 +885,15 @@ "message": "Duo Mobile, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ U2F സുരക്ഷാ കീ ഉപയോഗിച്ച് നിങ്ങളുടെ ഓർഗനൈസേഷനെ Duo Security ഉപയോഗിച്ച് പരിശോധിക്കുക.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 323d0cd3f7b..4f18a88d994 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index c1713e0bb32..46bfc5b7ee3 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index f264bb5f092..8c181361b45 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Feil" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Verifiser med Duo Security for din organisasjon gjennom Duo Mobile-appen, SMS, telefonsamtale, eller en U2F-sikkerhetsnøkkel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index fb8d7ec7df5..5aefc2febf4 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -249,6 +249,20 @@ "error": { "message": "त्रुटि" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "जनवरी" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index a23ef820ab7..e0f5a27a5c6 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Fout" }, + "decryptionError": { + "message": "Ontsleutelingsfout" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kon de onderstaande kluisitem(s) niet ontsleutelen." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Neem contact op met de klantenservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "om extra dataverlies te voorkomen.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "januari" }, @@ -871,6 +885,15 @@ "message": "Verificatie met Duo Security middels de Duo Mobile-app, sms, spraakoproep of een U2F-beveiligingssleutel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Controleer je identiteit" + }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Zwak wachtwoord geïdentificeerd en gevonden in een datalek. Gebruik een sterk en uniek wachtwoord om je account te beschermen. Weet je zeker dat je dit wachtwoord wilt gebruiken?" }, + "useThisPassword": { + "message": "Dit wachtwoord gebruiken" + }, + "useThisUsername": { + "message": "Deze gebruikersnaam gebruiken" + }, "checkForBreaches": { "message": "Bekende datalekken voor dit wachtwoord controleren" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Er zijn geen vrije poorten gevonden voor de sso-login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat pincode of wachtwoordontgrendeling eerst vereist is." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat het niet is ingeschakeld voor $EMAIL$ in de Bitwarden-desktopapp.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar om een onbekende reden." + }, "authorize": { "message": "Autoriseren" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "E-mailadres van het account veranderen" + }, + "organizationUpgradeRequired": { + "message": "Organisatie-upgrade vereist" + }, + "upgradeOrganization": { + "message": "Organisatie upgraden" + }, + "upgradeOrganizationDesc": { + "message": "Deze mogelijkheid is niet beschikbaar voor gratis organisaties. Schakel over naar een betaald abonnement om meer mogelijkheden te ontgrendelen." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extensie-uupdate vereist" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "De browserextensie die je gebruikt is verouderd. Werk deze bij of schakel de vingerafdruk validatie van de browserintegratie uit in instellingen van de desktop-app." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 543f20fc01d..74eabeefa2e 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Feil" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 2723db4f882..82404647e27 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -249,6 +249,20 @@ "error": { "message": "ତ୍ରୁଟି" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "ଜାନୁଆରୀ" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 9cddf815463..c40ef452d49 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Błąd" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Styczeń" }, @@ -871,6 +885,15 @@ "message": "Weryfikacja dostępu do Twojej organizacji z użyciem Duo Security poprzez aplikację Duo Mobile, SMS, połączenie telefoniczne lub klucz bezpieczeństwa U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Słabe hasło ujawnione w wyniku naruszenia ochrony danych. Użyj silnego i unikalnego hasła, aby chronić swoje konto. Czy na pewno chcesz użyć tego hasła?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Sprawdź znane naruszenia ochrony danych tego hasła" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Nie znaleziono wolnych portów dla logowania SSO." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 3e1b8f54040..71818b81b4b 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Janeiro" }, @@ -871,6 +885,15 @@ "message": "Verifique com o Duo Security utilizando o aplicativo Duo Mobile, SMS, chamada telefônica, ou chave de segurança U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "WebAuthn FIDO2" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Verificar vazamentos de dados conhecidos para esta senha" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Nenhuma porta livre foi encontrada para o cliente final." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Autorizar" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Alterar e-mail" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 55b031690a8..86d8d33144f 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Erro de desencriptação" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "O Bitwarden não conseguiu desencriptar o(s) item(ns) do cofre listado(s) abaixo." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacte o serviço de apoio ao cliente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "para evitar perdas adicionais de dados.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Janeiro" }, @@ -871,6 +885,15 @@ "message": "Proteja a sua organização com a Duo Security utilizando a aplicação Duo Mobile, SMS, chamada telefónica ou uma chave de segurança U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifique a sua identidade" + }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2787,7 +2810,7 @@ "message": "Recusar início de sessão" }, "logInConfirmedForEmailOnDevice": { - "message": "Início de sessão confirmado para $EMAIL$ em $DEVICE$", + "message": "Início de sessão confirmado para $EMAIL$ no $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -2800,7 +2823,7 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Negou uma tentativa de início de sessão de outro dispositivo. Se foi realmente o caso, tente iniciar sessão com o dispositivo novamente." + "message": "Recusou uma tentativa de início de sessão de outro dispositivo. Se foi realmente o caso, tente iniciar sessão com o dispositivo novamente." }, "justNow": { "message": "Agora mesmo" @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Palavra-passe fraca identificada e encontrada numa violação de dados. Utilize uma palavra-passe forte e única para proteger a sua conta. Tem a certeza de que pretende utilizar esta palavra-passe?" }, + "useThisPassword": { + "message": "Utilizar esta palavra-passe" + }, + "useThisUsername": { + "message": "Utilizar este nome de utilizador" + }, "checkForBreaches": { "message": "Verificar violações de dados conhecidas para esta palavra-passe" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Não foi possível encontrar portas livres para o início de sessão sso." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "O desbloqueio biométrico não está disponível porque o desbloqueio por PIN ou palavra-passe é necessário primeiro." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "O desbloqueio biométrico está atualmente indisponível." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "O desbloqueio biométrico não está disponível porque não está ativado para $EMAIL$ na app Bitwarden para computador.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "O desbloqueio biométrico está atualmente indisponível por um motivo desconhecido." + }, "authorize": { "message": "Autorizar" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Alterar o e-mail da conta" + }, + "organizationUpgradeRequired": { + "message": "Atualização da organização necessária" + }, + "upgradeOrganization": { + "message": "Atualizar organização" + }, + "upgradeOrganizationDesc": { + "message": "Esta funcionalidade não está disponível para organizações gratuitas. Mude para um plano pago para desbloquear mais funcionalidades." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Atualização da extensão necessária" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "A extensão do navegador que está a utilizar está desatualizada. Atualize-a ou desative a validação de impressões digitais da integração do navegador nas definições da aplicação para computador." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 0d26dbb2fc8..a9d3f1d6109 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Eroare" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "ianuarie" }, @@ -871,6 +885,15 @@ "message": "Verificați cu Duo Security pentru organizația dvs. utilizând aplicația Duo Mobile, SMS, apel telefonic sau cheia de securitate U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 056c9d32aca..d8b741d4cd4 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Ошибка" }, + "decryptionError": { + "message": "Ошибка расшифровки" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не удалось расшифровать элемент(ы) хранилища, перечисленные ниже." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Обратитесь в службу поддержки,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "чтобы избежать дополнительной потери данных.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Январь" }, @@ -871,6 +885,15 @@ "message": "Подтвердите с помощью Duo Security для вашей организации, используя приложение Duo Mobile, SMS, телефонный звонок или ключ безопасности U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Подтвердите вашу личность" + }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Обнаружен слабый пароль, найденный в утечке данных. Используйте надежный и уникальный пароль для защиты вашего аккаунта. Вы уверены, что хотите использовать этот пароль?" }, + "useThisPassword": { + "message": "Использовать этот пароль" + }, + "useThisUsername": { + "message": "Использовать это имя пользователя" + }, "checkForBreaches": { "message": "Проверять известные случаи утечки данных для этого пароля" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Не удалось найти свободные порты для авторизации SSO." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Биометрическая разблокировка недоступна, поскольку сначала требуется разблокировка с помощью PIN-кода или пароля." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Биометрическая разблокировка в настоящее время недоступна." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Биометрическая разблокировка недоступна, потому что она не включена для $EMAIL$ в приложении Bitwarden для компьютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Биометрическая разблокировка в настоящее время недоступна по неизвестной причине." + }, "authorize": { "message": "Разрешить" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Изменить email аккаунта" + }, + "organizationUpgradeRequired": { + "message": "Требуется обновление организации" + }, + "upgradeOrganization": { + "message": "Обновить организацию" + }, + "upgradeOrganizationDesc": { + "message": "Эта функция недоступна для бесплатных организаций. Переключитесь на платный план, чтобы разблокировать дополнительные возможности." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Необходимо обновить расширение" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Используемое вами расширение браузера устарело. Пожалуйста, обновите его или отключите проверку интеграции браузера с помощью отпечатка пальца в настройках приложения для компьютера." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 51625639dda..eca0b7b9eb7 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -249,6 +249,20 @@ "error": { "message": "දෝෂය" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "දුරුතු" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index cec06c3c028..d02bd35546d 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Chyba" }, + "decryptionError": { + "message": "Chyba dešifrovania" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nedokázal dešifrovať nižšie uvedené položky trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznícku podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "aby ste predišli ďalším stratám údajov.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Január" }, @@ -871,6 +885,15 @@ "message": "Overiť sa prostredníctvom Duo Security vašej organizácie použitím Duo Mobile aplikácie, SMS, telefonátu alebo U2F bezpečnostným kľúčom.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Overte svoju totožnosť" + }, + "weDontRecognizeThisDevice": { + "message": "Toto zariadenie nepoznáme. Na overenie vašej totožnosti zadajte kód, ktorý bol zaslaný na váš e-mail." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Nájdené slabé heslo v uniknuných údajoch. Na ochranu svojho účtu používajte silné a jedinečné heslo. Naozaj chcete používať toto heslo?" }, + "useThisPassword": { + "message": "Použiť toto heslo" + }, + "useThisUsername": { + "message": "Použiť toto používateľské meno" + }, "checkForBreaches": { "message": "Skontrolovať známe úniky údajov pre toto heslo" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Pre prihlásenie SSO sa nepodarilo nájsť žiadne voľné porty." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné pretože je najskôr potrebné odomykanie pomocou PIN alebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Odomykanie biometrickými údajmi je momentálne nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Odomykanie biometrickými údajmi je nedostupné pretože nie je povolené pre $EMAIL$ v aplikácii Bitwarden pre desktop.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Odomykanie biometrickými údajmi je z neznámych dôvodov nedostupné." + }, "authorize": { "message": "Autorizovať" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Zmeniť e-mail účtu" + }, + "organizationUpgradeRequired": { + "message": "Vyžaduje sa upgrade organizácie" + }, + "upgradeOrganization": { + "message": "Upgrade organizácie" + }, + "upgradeOrganizationDesc": { + "message": "Táto funkcia nie je k dispozícii pre bezplatné organizácie. Ak chcete odomknúť ďalšie funkcie, prejdite na platený plán." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Rozšírenie musíte aktualizovať" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Rozšírenie prehliadača, ktoré používate, je zastarané. Aktualizujte ho alebo zakážte overenie odtlačkov integrácie prehliadača v nastaveniach desktopovej aplikácie." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 1973f6c017b..d5c26aa266f 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Napaka" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januar" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 9f9dec2dd68..a647521a096 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Грешка" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Јануар" }, @@ -871,6 +885,15 @@ "message": "Провери са Duo Security за вашу организацију користећи Duo Mobile апликацију, СМС, телефонски позив, или U2F кључ.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Идентификована је слаба лозинка и пронађена у упаду података. Користите јаку и јединствену лозинку да заштитите свој налог. Да ли сте сигурни да желите да користите ову лозинку?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Проверите познате упада података за ову лозинку" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Нису пронађени портови за SSO пријаву." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Ауторизуј" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index c9a463246dd..2db2418aae0 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Fel" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Januari" }, @@ -871,6 +885,15 @@ "message": "Verifiera med Duo Security för din organisation genom att använda Duo Mobile-appen, SMS, telefonsamtal eller en U2F-säkerhetsnyckel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Lösenordet är svagt och avslöjades vid ett dataintrång. Använd ett starkt och unikt lösenord för att skydda ditt konto. Är det säkert att du vill använda detta lösenord?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Kontrollera kända dataintrång för detta lösenord" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 323d0cd3f7b..4f18a88d994 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "January" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 16961afcc57..a2b93eb4086 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -249,6 +249,20 @@ "error": { "message": "ข้อผิดพลาด" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "มกราคม" }, @@ -871,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 8357f8616bc..b5ec934712d 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Hata" }, + "decryptionError": { + "message": "Şifre çözme sorunu" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Ocak" }, @@ -871,6 +885,15 @@ "message": "Kuruluşunuzun Duo Security doğrulaması için Duo Mobile uygulaması, SMS, telefon görüşmesi veya U2F güvenlik anahtarını kullanın.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Kimliğinizi doğrulayın" + }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Hem zayıf hem de veri ihlalinde yer alan bir tespit edildi. Hesabınızı korumak için güçlü bir parola seçin ve o parolayı başka yerlerde kullanmayın. Bu parolayı kullanmak istediğinizden emin misiniz?" }, + "useThisPassword": { + "message": "Bu parolayı kullan" + }, + "useThisUsername": { + "message": "Bu kullanıcı adını kullan" + }, "checkForBreaches": { "message": "Bilinen veri ihlallerinde bu parolayı kontrol et" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "SSO girişi için açık port bulunamadı." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Yetkilendir" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Hesap e-postasını değiştir" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Uzantıyı güncellemeniz gerekiyor" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Kullandığınız tarayı uzantısı eskimiş. Lütfen uzantıyı güncelleyin veya masaüstü uygulamasının ayarlarından parmak izi doğrulaması için tarayıcı entegrasyonunu kapatın." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 4103e8b28a6..de8fd8664ca 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Помилка" }, + "decryptionError": { + "message": "Помилка розшифрування" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Зверніться до служби підтримки клієнтів,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "щоб уникнути втрати даних.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Січень" }, @@ -871,6 +885,15 @@ "message": "Авторизуйтесь за допомогою Duo Security для вашої організації з використанням програми Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Підтвердьте свою особу" + }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Виявлено слабкий пароль, який знайдено у витоку даних. Використовуйте надійний та унікальний пароль для захисту свого облікового запису. Ви дійсно хочете використати цей пароль?" }, + "useThisPassword": { + "message": "Використати цей пароль" + }, + "useThisUsername": { + "message": "Використати це ім'я користувача" + }, "checkForBreaches": { "message": "Перевірити відомі витоки даних для цього пароля" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Не знайдено вільних портів для цього входу SSO." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Біометричне розблокування недоступне, оскільки спочатку потрібно ввести PIN-код або пароль." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Біометричне розблокування наразі недоступне." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Біометричне розблокування недоступне, оскільки воно не увімкнене для $EMAIL$ у програмі Bitwarden для комп'ютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Біометричне розблокування зараз недоступне з невідомої причини." + }, "authorize": { "message": "Авторизувати" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Змінити адресу е-пошти" + }, + "organizationUpgradeRequired": { + "message": "Необхідне оновлення організації" + }, + "upgradeOrganization": { + "message": "Підвищити рівень організації" + }, + "upgradeOrganizationDesc": { + "message": "Ця функція недоступна для безплатних організацій. Передплатіть тарифний план, щоб розблокувати додаткові можливості." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Необхідно оновити розширення" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Ви використовуєте застарілу версію розширення браузера. Оновіть його або вимкніть перевірку цифрового відбитка інтеграції з браузером у налаштуваннях комп'ютерної програми." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index e514124414b..4f996404b8a 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -249,6 +249,20 @@ "error": { "message": "Lỗi" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "Tháng 1" }, @@ -871,6 +885,15 @@ "message": "Xác minh với Duo Security cho tổ chức của bạn sử dụng ứng dụng Duo Mobile, SMS, cuộc gọi điện thoại, hoặc khoá bảo mật U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Mật khẩu yếu này đã bị rò rỉ trong một vụ tấn công dữ liệu. Dùng mật khẩu mới và an toàn để bảo vệ tài khoản bạn. Bạn có chắc muốn sử dụng mật khẩu đã bị rò rỉ?" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, "checkForBreaches": { "message": "Kiểm tra mật khẩu có lộ trong các vụ rò rỉ dữ liệu hay không" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "Không thể tìm thấy cổng trống để đăng nhập SSO." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authorize": { "message": "Authorize" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 9bff79aa857..53b9b5ccb47 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -249,6 +249,20 @@ "error": { "message": "错误" }, + "decryptionError": { + "message": "解密错误" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 无法解密下列密码库项目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "联系客户成功团队", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "以避免额外的数据丢失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "一月" }, @@ -871,6 +885,15 @@ "message": "为您的组织使用 Duo Security 的 Duo 移动 App、短信、电话或 U2F 安全钥匙来进行验证。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "验证您的身份" + }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -975,13 +998,13 @@ "message": "您的登录会话已过期。" }, "restartRegistration": { - "message": "重新开始注册" + "message": "重启注册" }, "expiredLink": { "message": "失效链接" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "请重新注册或尝试登录。" + "message": "请重启注册或尝试登录。" }, "youMayAlreadyHaveAnAccount": { "message": "您可能已经有一个账户了" @@ -1008,7 +1031,7 @@ "message": "账户" }, "loading": { - "message": "加载中…" + "message": "正在加载..." }, "lockVault": { "message": "锁定密码库" @@ -1753,7 +1776,7 @@ "message": "应用程序启动时要求使用触控 ID" }, "requirePasswordOnStart": { - "message": "App 启动时要求输入密码或 PIN 码" + "message": "应用程序启动时要求输入密码或 PIN 码" }, "recommendedForSecurity": { "message": "安全起见,推荐设置。" @@ -2111,11 +2134,11 @@ "message": "当前访问次数" }, "disableSend": { - "message": "停用此 Send 则任何人无法访问它。", + "message": "停用此 Send 确保无人能访问它。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "可选,用户需要提供密码才能访问此 Send。", + "message": "可选。用户需要提供密码才能访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2158,10 +2181,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { - "message": "您想要发送的文本。" + "message": "您想在此 Send 中附加的文本。" }, "sendFileDesc": { - "message": "您想要发送的文件。" + "message": "您想在此 Send 中附加的文件。" }, "days": { "message": "$DAYS$ 天", @@ -2255,7 +2278,7 @@ "message": "更新主密码" }, "updateMasterPasswordWarning": { - "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" + "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" @@ -2495,7 +2518,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个字符生成强大的密码。", + "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 +2528,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个单词生成强大的密码短语。", + "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": { @@ -2865,6 +2888,12 @@ "weakAndBreachedMasterPasswordDesc": { "message": "识别到弱密码且其出现在数据泄露中。请使用一个强且唯一的密码以保护你的账户。确定要使用这个密码吗?" }, + "useThisPassword": { + "message": "使用此密码" + }, + "useThisUsername": { + "message": "使用此用户名" + }, "checkForBreaches": { "message": "检查已知的数据泄露是否包含此密码" }, @@ -3362,6 +3391,30 @@ "ssoError": { "message": "找不到用于 SSO 登录的可用端口。" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "生物识别解锁不可用,因为需要先使用 PIN 或密码解锁。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物识别解锁当前不可用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "生物识别解锁不可用,因为在 Bitwarden 桌面 App 中没有为 $EMAIL$ 启用生物识别解锁。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "由于某个未知的原因,生物识别解锁当前不可用。" + }, "authorize": { "message": "批准" }, @@ -3430,5 +3483,20 @@ }, "changeAcctEmail": { "message": "更改账户电子邮箱" + }, + "organizationUpgradeRequired": { + "message": "需要升级组织" + }, + "upgradeOrganization": { + "message": "升级组织" + }, + "upgradeOrganizationDesc": { + "message": "此功能不适用于免费组织。请切换到付费计划以解锁更多功能。" + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "扩展需要更新" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "您正在使用的浏览器扩展已过时。请更新它或在桌面 App 的设置中禁用浏览器集成指纹验证。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 3c7884a22c5..ecbdf0cbf96 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -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" @@ -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金鑰的請求" @@ -229,7 +229,7 @@ "message": "SSH金鑰請求超時" }, "enableSshAgent": { - "message": "啟用SSH代理" + "message": "啟用 SSH 代理程式" }, "enableSshAgentDesc": { "message": "啟用SSBitwardenH代理以從 Bitwarden 密碼庫簽發SSH請求" @@ -249,6 +249,20 @@ "error": { "message": "錯誤" }, + "decryptionError": { + "message": "解密發生錯誤" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 無法解密您密碼庫中下面的項目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "聯絡客戶支援部門", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "來避免更多資料遺失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "january": { "message": "一月" }, @@ -510,7 +524,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "包含小寫字元", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -642,10 +656,10 @@ "message": "以通行密鑰 (passkey) 登入" }, "loginWithDevice": { - "message": "Log in with device" + "message": "使用裝置登入" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "使用單一登入" }, "submit": { "message": "送出" @@ -666,7 +680,7 @@ "message": "主密碼提示(選用)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "如果您忘記了密碼,可以傳送密碼提示到您的電子郵件。$CURRENT$ / 最多 $MAXIMUM$ 個字元", "placeholders": { "current": { "content": "$1", @@ -703,7 +717,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "設定主密碼以完成加入這個組織" }, "settings": { "message": "設定" @@ -712,13 +726,13 @@ "message": "電子郵件帳戶:" }, "requestHint": { - "message": "Request hint" + "message": "請求提示" }, "requestPasswordHint": { "message": "主密碼提示" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "輸入您帳號的電子郵件,您的密碼提示會傳送給您" }, "passwordHint": { "message": "密碼提示" @@ -764,7 +778,7 @@ "message": "帳戶已建立!現在可以登入了。" }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "您已成功建立新帳號!" }, "youHaveBeenLoggedIn": { "message": "你已經登入!" @@ -854,23 +868,32 @@ "message": "驗證器應用程式" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "輸入驗證器應用程式產生的驗證碼,例如 Bitwarden 驗證器。", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "YubiKey OTP 安全金鑰" }, "yubiKeyDesc": { "message": "使用 YubiKey 來存取您的帳戶。支援 YubiKey 4、4 Nano、4C、以及 NEO 裝置。" }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "輸入 Duo 應用程式產生的驗證碼。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { "message": "爲您的組織使用 Duo Security 的 Duo Mobile 程式、SMS、致電或 U2F 安全鑰匙進行驗證。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -881,7 +904,7 @@ "message": "電子郵件" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "輸入寄送到您電子郵件信箱的驗證碼。" }, "loginUnavailable": { "message": "登入無法使用" @@ -902,13 +925,13 @@ "message": "指定您本地托管的 Bitwarden 安裝之基礎 URL。" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "指定您自建的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "您必須新增伺服器網域 URL 或至少一個自定義環境。" }, "customEnvironment": { "message": "自訂環境" @@ -920,13 +943,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": { @@ -969,22 +992,22 @@ "message": "已登出" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "您已經登出了您的帳號。" }, "loginExpired": { "message": "您的登入會話已過期。" }, "restartRegistration": { - "message": "Restart registration" + "message": "重新啟動註冊" }, "expiredLink": { - "message": "Expired link" + "message": "過期連結" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "請重新啟動註冊流程或是重試登入。" }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "您可能已經有帳號" }, "logOutConfirmation": { "message": "您確定要登出嗎?" @@ -1041,10 +1064,10 @@ "message": "變更主密碼" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "前往網頁應用程式嗎?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "您可以在 Bitwarden 網頁應用程式上變更主密碼。" }, "fingerprintPhrase": { "message": "指紋短語", @@ -1073,16 +1096,16 @@ "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "您的帳戶已被鎖定。" }, "or": { - "message": "or" + "message": "或" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "使用生物辨識解鎖" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "使用主密碼解鎖" }, "unlock": { "message": "解鎖" @@ -1113,7 +1136,7 @@ "message": "密碼庫逾時時間" }, "vaultTimeout1": { - "message": "Timeout" + "message": "逾時" }, "vaultTimeoutDesc": { "message": "選擇密碼庫何時執行密碼庫逾時動作。" @@ -1320,7 +1343,7 @@ "description": "Copy credit card number" }, "copyEmail": { - "message": "Copy email" + "message": "複製電子郵件地址" }, "copySecurityCode": { "message": "複製安全代碼", @@ -1369,7 +1392,7 @@ "message": "您可以在 bitwarden.com 網頁版密碼庫購買進階會員資格。現在要前往嗎?" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "您可以在 Bitwarden 網頁應用程式的帳號設定中購買進階版。" }, "premiumCurrentMember": { "message": "您目前是進階會員!" @@ -1393,13 +1416,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 +1432,13 @@ "message": "沒有可列出的密碼。" }, "clearHistory": { - "message": "Clear history" + "message": "清除歷史紀錄" }, "nothingToShow": { - "message": "Nothing to show" + "message": "沒有可顯示的內容" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "您最近未產生任何密碼" }, "undo": { "message": "復原" @@ -1492,13 +1515,13 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "複製成功" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "存取權杖更新失敗" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "未找到存取權杖或 API 密鑰。請重試登出再登入。" }, "help": { "message": "說明" @@ -1588,7 +1611,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "匯出自" }, "exportVault": { "message": "匯出密碼庫" @@ -1720,7 +1743,7 @@ "message": "無效的 PIN 碼。" }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "輸入太多無效 PIN 碼。 正在登出。" }, "unlockWithWindowsHello": { "message": "使用 Windows Hello 解鎖" @@ -1729,7 +1752,7 @@ "message": "額外的 Windows Hello 設定" }, "unlockWithPolkit": { - "message": "Unlock with system authentication" + "message": "使用系統驗證解鎖" }, "windowsHelloConsentMessage": { "message": "驗證 Bitwarden。" @@ -1747,7 +1770,7 @@ "message": "啟動應用程式時詢問 Windows Hello" }, "autoPromptPolkit": { - "message": "Ask for system authentication on launch" + "message": "在啟動時詢問系統驗證" }, "autoPromptTouchId": { "message": "啟動應用程式時要求 Touch ID" @@ -1759,7 +1782,7 @@ "message": "為提升安全,建議使用。" }, "lockWithMasterPassOnRestart1": { - "message": "Lock with master password on restart" + "message": "重啟後使用主密碼鎖定" }, "deleteAccount": { "message": "刪除帳戶" @@ -1771,7 +1794,7 @@ "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." @@ -2726,13 +2749,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" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "需要另一個選項嗎?" }, "fingerprintMatchInfo": { "message": "請確保您的密碼庫已解鎖,並且指紋短語與其他裝置的一致。" @@ -2747,7 +2770,7 @@ "message": "必須先在 Bitwarden 應用程式設定中開啟,才可以使用裝置登入。要改用其他選項嗎?" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "viewAllLoginOptions": { "message": "檢視所有登入選項" @@ -2848,7 +2871,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "返回" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -2865,11 +2888,17 @@ "weakAndBreachedMasterPasswordDesc": { "message": "密碼強度不足,且該密碼已洩露。請使用一個強度足夠和獨特的密碼來保護您的帳戶。您確定要使用這個密碼嗎?" }, + "useThisPassword": { + "message": "使用此密碼" + }, + "useThisUsername": { + "message": "使用此使用者名稱" + }, "checkForBreaches": { "message": "檢查外洩的密碼資料庫中是否包含此密碼" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "已登入!" }, "important": { "message": "重要:" @@ -2908,10 +2937,10 @@ "message": "裝置需要取得核准。請在下面選擇一個核准選項:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "需要核准裝置" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "選擇下面的一個核准選項" }, "rememberThisDevice": { "message": "記住這個裝置" @@ -2966,7 +2995,7 @@ "message": "缺少使用者電子郵件地址" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "未找到使用中帳號的電子郵件。正在將您登出。" }, "deviceTrusted": { "message": "裝置已信任" @@ -3072,7 +3101,7 @@ "message": "子選單" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "切換側邊欄" }, "skipToContent": { "message": "跳至內容" @@ -3130,16 +3159,16 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "連接到 Duo 服務時發生錯誤。使用不同的兩階段認證或聯繫 Duo 來獲得支援。" }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "您的帳號要求使用 Duo 兩步驟驗證登入。" }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "使用瀏覽器啟動 Duo" }, "importFormatError": { "message": "資料格式不正確。請檢查匯入檔案後再試一次。" @@ -3154,7 +3183,7 @@ "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, "destination": { - "message": "Destination" + "message": "目的" }, "learnAboutImportOptions": { "message": "瞭解更多匯入選項" @@ -3293,7 +3322,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "成功" }, "troubleshooting": { "message": "疑難排解" @@ -3311,13 +3340,13 @@ "message": "密碼金鑰已移除" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "指定目標集合時發生錯誤。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "指定目標資料夾時發生錯誤。" }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "檢視 $NAME$ 中的項目", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -3327,7 +3356,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "回到 $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -3337,11 +3366,11 @@ } }, "back": { - "message": "Back", + "message": "返回", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "移除 $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -3351,67 +3380,91 @@ } }, "data": { - "message": "Data" + "message": "資料" }, "fileSends": { - "message": "File Sends" + "message": "檔案 Send" }, "textSends": { - "message": "Text Sends" + "message": "文字 Sends" }, "ssoError": { - "message": "No free ports could be found for the sso login." + "message": "無法找到可用於 SSO 登入的空閒連接埠。" + }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "需要 PIN 碼或密碼解鎖才能使用生物辨識解鎖。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物辨識解鎖暫時無法使用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextNotEnabledLocally": { + "message": "由於未在 Bitwarden 桌面應用程式的 $EMAIL$ 帳號上啟動,生物辨識解鎖無法使用。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "基於不明原因,生物辨識解鎖無法使用。" }, "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": "File saved to device. Manage from your device downloads." + "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": "從 2025 年 2 月開始,Bitwarden 會傳送代碼到您的帳號電子郵件中來驗證新裝置的登入。" }, "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", @@ -3420,15 +3473,30 @@ } }, "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": "更改帳號電子郵件位址" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 6915a18deda..107d546811c 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -405,6 +405,8 @@ export class NativeMessagingMain { this.logService.info(`Error reading preferences: ${e}`); } } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Browser is not installed, we can just skip it } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index f93d9059bc7..459785f59d3 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.1.0", + "version": "2025.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.1.0", + "version": "2025.1.8", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 39e16815020..db546590ba5 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.1.0", + "version": "2025.1.8", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/components/approve-ssh-request.ts b/apps/desktop/src/platform/components/approve-ssh-request.ts index 62200962dca..0443034f551 100644 --- a/apps/desktop/src/platform/components/approve-ssh-request.ts +++ b/apps/desktop/src/platform/components/approve-ssh-request.ts @@ -10,8 +10,8 @@ import { DialogModule, FormFieldModule, IconButtonModule, + DialogService, } from "@bitwarden/components"; -import { DialogService } from "@bitwarden/components/src/dialog"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; export interface ApproveSshRequestParams { diff --git a/apps/desktop/src/platform/flags.ts b/apps/desktop/src/platform/flags.ts index dc0103e2436..a762053da35 100644 --- a/apps/desktop/src/platform/flags.ts +++ b/apps/desktop/src/platform/flags.ts @@ -7,11 +7,9 @@ import { } from "@bitwarden/common/platform/misc/flags"; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = {} & SharedFlags; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = {} & SharedDevFlags; export function flagEnabled(flag: keyof Flags): boolean { diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index a37274677ff..24360ecbbb5 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -123,6 +123,7 @@ const ephemeralStore = { getEphemeralValue: (key: string): Promise => ipcRenderer.invoke("getEphemeralValue", key), removeEphemeralValue: (key: string): Promise => ipcRenderer.invoke("deleteEphemeralValue", key), + listEphemeralValueKeys: (): Promise => ipcRenderer.invoke("listEphemeralValueKeys"), }; const localhostCallbackService = { diff --git a/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts index b59b48be1e1..0dd6ef56ae4 100644 --- a/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts +++ b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts @@ -17,5 +17,8 @@ export class EphemeralValueStorageService { ipcMain.handle("deleteEphemeralValue", async (event, key: string) => { this.ephemeralValues.delete(key); }); + ipcMain.handle("listEphemeralValueKeys", async (event) => { + return Array.from(this.ephemeralValues.keys()); + }); } } diff --git a/apps/desktop/src/platform/services/ssh-agent.service.ts b/apps/desktop/src/platform/services/ssh-agent.service.ts index e860ebe1db5..d4c7c5f460e 100644 --- a/apps/desktop/src/platform/services/ssh-agent.service.ts +++ b/apps/desktop/src/platform/services/ssh-agent.service.ts @@ -45,6 +45,8 @@ export class SshAgentService implements OnDestroy { SSH_VAULT_UNLOCK_REQUEST_TIMEOUT = 60_000; SSH_REQUEST_UNLOCK_POLLING_INTERVAL = 100; + private isFeatureFlagEnabled = false; + private destroy$ = new Subject(); constructor( @@ -65,21 +67,31 @@ export class SshAgentService implements OnDestroy { .getFeatureFlag$(FeatureFlag.SSHAgent) .pipe( concatMap(async (enabled) => { - if (enabled && !(await ipc.platform.sshAgent.isLoaded())) { - return this.initSshAgent(); + this.isFeatureFlagEnabled = enabled; + if (!(await ipc.platform.sshAgent.isLoaded()) && enabled) { + await ipc.platform.sshAgent.init(); } }), takeUntil(this.destroy$), ) .subscribe(); - } - private async initSshAgent() { - await ipc.platform.sshAgent.init(); + await this.initListeners(); + } + private async initListeners() { this.messageListener .messages$(new CommandDefinition("sshagent.signrequest")) .pipe( + withLatestFrom(this.desktopSettingsService.sshAgentEnabled$), + concatMap(async ([message, enabled]) => { + if (!enabled) { + await ipc.platform.sshAgent.signRequestResponse(message.requestId as number, false); + } + return { message, enabled }; + }), + filter(({ enabled }) => enabled), + map(({ message }) => message), withLatestFrom(this.authService.activeAccountStatus$), // This switchMap handles unlocking the vault if it is locked: // - If the vault is locked, we will wait for it to be unlocked. @@ -179,18 +191,30 @@ export class SshAgentService implements OnDestroy { this.accountService.activeAccount$.pipe(skip(1), takeUntil(this.destroy$)).subscribe({ next: (account) => { + if (!this.isFeatureFlagEnabled) { + return; + } + this.logService.info("Active account changed, clearing SSH keys"); ipc.platform.sshAgent .clearKeys() .catch((e) => this.logService.error("Failed to clear SSH keys", e)); }, error: (e: unknown) => { + if (!this.isFeatureFlagEnabled) { + return; + } + this.logService.error("Error in active account observable", e); ipc.platform.sshAgent .clearKeys() .catch((e) => this.logService.error("Failed to clear SSH keys", e)); }, complete: () => { + if (!this.isFeatureFlagEnabled) { + return; + } + this.logService.info("Active account observable completed, clearing SSH keys"); ipc.platform.sshAgent .clearKeys() @@ -204,11 +228,23 @@ export class SshAgentService implements OnDestroy { ]) .pipe( concatMap(async ([, enabled]) => { + if (!this.isFeatureFlagEnabled) { + return; + } + if (!enabled) { await ipc.platform.sshAgent.clearKeys(); return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + const authStatus = await firstValueFrom( + this.authService.authStatusFor$(activeAccount.id), + ); + if (authStatus !== AuthenticationStatus.Unlocked) { + return; + } + const ciphers = await this.cipherService.getAllDecrypted(); if (ciphers == null) { await ipc.platform.sshAgent.lock(); diff --git a/apps/desktop/src/services/biometric-message-handler.service.spec.ts b/apps/desktop/src/services/biometric-message-handler.service.spec.ts index 13b668f6b83..a7f0f555ca2 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.spec.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.spec.ts @@ -9,21 +9,22 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService } from "@bitwarden/common/spec"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; -import { DialogService } from "@bitwarden/components"; -import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management"; +import { DialogService, I18nMockService } from "@bitwarden/components"; +import { + KeyService, + BiometricsService, + BiometricStateService, + BiometricsCommands, +} from "@bitwarden/key-management"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; import { BiometricMessageHandlerService } from "./biometric-message-handler.service"; -(global as any).ipc = { - platform: { - reloadProcess: jest.fn(), - }, -}; - const SomeUser = "SomeUser" as UserId; const AnotherUser = "SomeOtherUser" as UserId; const accounts = { @@ -54,6 +55,7 @@ describe("BiometricMessageHandlerService", () => { let accountService: AccountService; let authService: MockProxy; let ngZone: MockProxy; + let i18nService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); @@ -69,6 +71,24 @@ describe("BiometricMessageHandlerService", () => { accountService = new FakeAccountService(accounts); authService = mock(); ngZone = mock(); + i18nService = mock(); + + (global as any).ipc = { + platform: { + ephemeralStore: { + listEphemeralValueKeys: jest.fn(), + getEphemeralValue: jest.fn(), + removeEphemeralValue: jest.fn(), + setEphemeralValue: jest.fn(), + }, + nativeMessaging: { + sendMessage: jest.fn(), + }, + reloadProcess: jest.fn(), + }, + }; + cryptoFunctionService.rsaEncrypt.mockResolvedValue(Utils.fromUtf8ToArray("encrypted")); + cryptoFunctionService.randomBytes.mockResolvedValue(new Uint8Array(64) as CsprngArray); service = new BiometricMessageHandlerService( cryptoFunctionService, @@ -83,9 +103,256 @@ describe("BiometricMessageHandlerService", () => { accountService, authService, ngZone, + i18nService, ); }); + describe("setup encryption", () => { + it("should reject when user is not in app", async () => { + await service.handleMessage({ + appId: "appId", + message: { + command: "setupEncryption", + messageId: 0, + userId: "unknownUser" as UserId, + }, + }); + expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + appId: "appId", + command: "wrongUserId", + }); + }); + + it("should setup secure communication", async () => { + (global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([ + "connectedApp_appId", + ]); + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue( + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: null, + trusted: false, + }), + ); + await service.handleMessage({ + appId: "appId", + message: { + command: "setupEncryption", + messageId: 0, + userId: SomeUser, + publicKey: Utils.fromUtf8ToB64("publicKey"), + }, + }); + expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + appId: "appId", + command: "setupEncryption", + messageId: -1, + sharedSecret: Utils.fromUtf8ToB64("encrypted"), + }); + expect((global as any).ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith( + "connectedApp_appId", + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: false, + }), + ); + }); + + it("should invalidate encryption if connection is not secured", async () => { + (global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([ + "connectedApp_appId", + ]); + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue( + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: null, + trusted: false, + }), + ); + await service.handleMessage({ + appId: "appId", + message: { + command: "biometricUnlock", + messageId: 0, + userId: SomeUser, + }, + }); + expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + appId: "appId", + command: "invalidateEncryption", + }); + }); + + it("should show update dialog when legacy unlock is requested with fingerprint active", async () => { + desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true); + (global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([ + "connectedApp_appId", + ]); + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue( + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: false, + }), + ); + encryptService.decryptToUtf8.mockResolvedValue( + JSON.stringify({ + command: "biometricUnlock", + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }), + ); + await service.handleMessage({ + appId: "appId", + message: { + command: "biometricUnlock", + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }, + }); + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + }); + + it("should send verify fingerprint when fingerprinting is required on modern unlock, and dialog is accepted, and set to trusted", async () => { + desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true); + (global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([ + "connectedApp_appId", + ]); + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue( + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: false, + }), + ); + ngZone.run.mockReturnValue({ + closed: of(true), + }); + encryptService.decryptToUtf8.mockResolvedValue( + JSON.stringify({ + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }), + ); + await service.handleMessage({ + appId: "appId", + message: { + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }, + }); + + expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + command: "verifyDesktopIPCFingerprint", + appId: "appId", + }); + expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + command: "verifiedDesktopIPCFingerprint", + appId: "appId", + }); + expect(ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith( + "connectedApp_appId", + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: true, + }), + ); + }); + + it("should send reject fingerprint when fingerprinting is required on modern unlock, and dialog is rejected, and it should not set to trusted", async () => { + desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true); + (global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([ + "connectedApp_appId", + ]); + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue( + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: false, + }), + ); + ngZone.run.mockReturnValue({ + closed: of(false), + }); + encryptService.decryptToUtf8.mockResolvedValue( + JSON.stringify({ + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }), + ); + await service.handleMessage({ + appId: "appId", + message: { + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }, + }); + expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + command: "verifyDesktopIPCFingerprint", + appId: "appId", + }); + expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ + command: "rejectedDesktopIPCFingerprint", + appId: "appId", + }); + expect(ipc.platform.ephemeralStore.setEphemeralValue).not.toHaveBeenCalledWith( + "connectedApp_appId", + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: true, + }), + ); + }); + + it("should not attempt to verify when the connected app is already trusted", async () => { + desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true); + (global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([ + "connectedApp_appId", + ]); + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue( + JSON.stringify({ + publicKey: Utils.fromUtf8ToB64("publicKey"), + sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)), + trusted: true, + }), + ); + encryptService.decryptToUtf8.mockResolvedValue( + JSON.stringify({ + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }), + ); + await service.handleMessage({ + appId: "appId", + message: { + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId: 0, + timestamp: Date.now(), + userId: SomeUser, + }, + }); + expect(ipc.platform.nativeMessaging.sendMessage).not.toHaveBeenCalledWith({ + command: "verifyDesktopIPCFingerprint", + appId: "appId", + }); + }); + }); + describe("process reload", () => { const testCases = [ // don't reload when the active user is the requested one and unlocked diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts index ea1e7e76c56..0482434708e 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.ts @@ -1,13 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable, NgZone } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { combineLatest, concatMap, firstValueFrom, map } from "rxjs"; 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 { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -31,6 +32,50 @@ import { DesktopSettingsService } from "../platform/services/desktop-settings.se const MessageValidTimeout = 10 * 1000; const HashAlgorithmForAsymmetricEncryption = "sha1"; +type ConnectedApp = { + publicKey: string; + sessionSecret: string; + trusted: boolean; +}; + +const ConnectedAppPrefix = "connectedApp_"; + +class ConnectedApps { + async get(appId: string): Promise { + if (!(await this.has(appId))) { + return null; + } + + return JSON.parse( + await ipc.platform.ephemeralStore.getEphemeralValue(`${ConnectedAppPrefix}${appId}`), + ); + } + + async list(): Promise { + return (await ipc.platform.ephemeralStore.listEphemeralValueKeys()) + .filter((key) => key.startsWith(ConnectedAppPrefix)) + .map((key) => key.replace(ConnectedAppPrefix, "")); + } + + async set(appId: string, value: ConnectedApp) { + await ipc.platform.ephemeralStore.setEphemeralValue( + `${ConnectedAppPrefix}${appId}`, + JSON.stringify(value), + ); + } + + async has(appId: string) { + return (await this.list()).find((id) => id === appId) != null; + } + + async clear() { + const connected = await this.list(); + for (const appId of connected) { + await ipc.platform.ephemeralStore.removeEphemeralValue(`${ConnectedAppPrefix}${appId}`); + } + } +} + @Injectable() export class BiometricMessageHandlerService { constructor( @@ -46,7 +91,21 @@ export class BiometricMessageHandlerService { private accountService: AccountService, private authService: AuthService, private ngZone: NgZone, - ) {} + private i18nService: I18nService, + ) { + combineLatest([ + this.desktopSettingService.browserIntegrationFingerprintEnabled$, + this.desktopSettingService.browserIntegrationEnabled$, + ]) + .pipe( + concatMap(async () => { + await this.connectedApps.clear(); + }), + ) + .subscribe(); + } + + private connectedApps: ConnectedApps = new ConnectedApps(); async handleMessage(msg: LegacyMessageWrapper) { const { appId, message: rawMessage } = msg as LegacyMessageWrapper; @@ -69,39 +128,24 @@ export class BiometricMessageHandlerService { return; } - if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) { - this.logService.info("[Native Messaging IPC] Requesting fingerprint verification."); - ipc.platform.nativeMessaging.sendMessage({ - command: "verifyFingerprint", - appId: appId, - }); - - const fingerprint = await this.keyService.getFingerprint( - rawMessage.userId, - remotePublicKey, - ); - - this.messagingService.send("setFocus"); - - const dialogRef = this.ngZone.run(() => - BrowserSyncVerificationDialogComponent.open(this.dialogService, { fingerprint }), + if (await this.connectedApps.has(appId)) { + this.logService.info( + "[Native Messaging IPC] Public key for app id changed. Invalidating trust", ); - - const browserSyncVerified = await firstValueFrom(dialogRef.closed); - - if (browserSyncVerified !== true) { - this.logService.info("[Native Messaging IPC] Fingerprint verification failed."); - return; - } } + await this.connectedApps.set(appId, { + publicKey: Utils.fromBufferToB64(remotePublicKey), + sessionSecret: null, + trusted: false, + }); await this.secureCommunication(remotePublicKey, appId); return; } - if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) { + if ((await this.connectedApps.get(appId))?.sessionSecret == null) { this.logService.info( - "[Native Messaging IPC] Epheremal secret for secure channel is missing. Invalidating encryption...", + "[Native Messaging IPC] Session secret for secure channel is missing. Invalidating encryption...", ); ipc.platform.nativeMessaging.sendMessage({ command: "invalidateEncryption", @@ -113,7 +157,7 @@ export class BiometricMessageHandlerService { const message: LegacyMessage = JSON.parse( await this.encryptService.decryptToUtf8( rawMessage as EncString, - SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), + SymmetricCryptoKey.fromString((await this.connectedApps.get(appId)).sessionSecret), ), ); @@ -188,7 +232,7 @@ export class BiometricMessageHandlerService { appId, ); } - // TODO: legacy, remove after 2025.01 + // TODO: legacy, remove after 2025.3 case BiometricsCommands.IsAvailable: { const available = (await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available; @@ -200,8 +244,20 @@ export class BiometricMessageHandlerService { appId, ); } - // TODO: legacy, remove after 2025.01 + // TODO: legacy, remove after 2025.3 case BiometricsCommands.Unlock: { + if ( + await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$) + ) { + await this.send({ command: "biometricUnlock", response: "not available" }, appId); + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("updateBrowserOrDisableFingerprintDialogTitle"), + content: this.i18nService.t("updateBrowserOrDisableFingerprintDialogMessage"), + type: "warning", + }); + return; + } + const isTemporarilyDisabled = (await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) && !((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available); @@ -265,6 +321,8 @@ export class BiometricMessageHandlerService { } else { await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } @@ -281,7 +339,7 @@ export class BiometricMessageHandlerService { const encrypted = await this.encryptService.encrypt( JSON.stringify(message), - SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), + SymmetricCryptoKey.fromString((await this.connectedApps.get(appId)).sessionSecret), ); ipc.platform.nativeMessaging.sendMessage({ @@ -293,10 +351,9 @@ export class BiometricMessageHandlerService { private async secureCommunication(remotePublicKey: Uint8Array, appId: string) { const secret = await this.cryptoFunctionService.randomBytes(64); - await ipc.platform.ephemeralStore.setEphemeralValue( - appId, - new SymmetricCryptoKey(secret).keyB64, - ); + const connectedApp = await this.connectedApps.get(appId); + connectedApp.sessionSecret = new SymmetricCryptoKey(secret).keyB64; + await this.connectedApps.set(appId, connectedApp); this.logService.info("[Native Messaging IPC] Setting up secure channel"); const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt( @@ -318,6 +375,18 @@ export class BiometricMessageHandlerService { appId: string, ) { const messageUserId = message.userId as UserId; + if (!(await this.validateFingerprint(appId))) { + await this.send( + { + command: BiometricsCommands.UnlockWithBiometricsForUser, + messageId, + response: false, + }, + appId, + ); + return; + } + try { const userKey = await this.biometricsService.unlockWithBiometricsForUser(messageUserId); if (userKey != null) { @@ -342,6 +411,8 @@ export class BiometricMessageHandlerService { appId, ); } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { await this.send( { command: BiometricsCommands.UnlockWithBiometricsForUser, messageId, response: false }, @@ -365,4 +436,54 @@ export class BiometricMessageHandlerService { } } } + + async validateFingerprint(appId: string): Promise { + if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) { + const appToValidate = await this.connectedApps.get(appId); + if (appToValidate == null) { + return false; + } + + if (appToValidate.trusted) { + return true; + } + + ipc.platform.nativeMessaging.sendMessage({ + command: "verifyDesktopIPCFingerprint", + appId: appId, + }); + + const fingerprint = await this.keyService.getFingerprint( + appId, + Utils.fromB64ToArray(appToValidate.publicKey), + ); + + this.messagingService.send("setFocus"); + + const dialogRef = this.ngZone.run(() => + BrowserSyncVerificationDialogComponent.open(this.dialogService, { fingerprint }), + ); + + const browserSyncVerified = await firstValueFrom(dialogRef.closed); + if (browserSyncVerified !== true) { + this.logService.info("[Native Messaging IPC] Fingerprint verification failed."); + ipc.platform.nativeMessaging.sendMessage({ + command: "rejectedDesktopIPCFingerprint", + appId: appId, + }); + return false; + } else { + this.logService.info("[Native Messaging IPC] Fingerprint verified."); + ipc.platform.nativeMessaging.sendMessage({ + command: "verifiedDesktopIPCFingerprint", + appId: appId, + }); + } + + appToValidate.trusted = true; + await this.connectedApps.set(appId, appToValidate); + } + + return true; + } } diff --git a/apps/desktop/src/services/duckduckgo-message-handler.service.ts b/apps/desktop/src/services/duckduckgo-message-handler.service.ts index 9a914f238b5..fa5c2f4d9f7 100644 --- a/apps/desktop/src/services/duckduckgo-message-handler.service.ts +++ b/apps/desktop/src/services/duckduckgo-message-handler.service.ts @@ -130,6 +130,8 @@ export class DuckDuckGoMessageHandlerService { sharedKey: Utils.fromBufferToB64(encryptedSecret), }, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { this.sendResponse({ messageId: messageId, @@ -153,6 +155,8 @@ export class DuckDuckGoMessageHandlerService { await this.encryptedMessageHandlerService.responseDataForCommand(decryptedCommandData); await this.sendEncryptedResponse(message, { command, payload: responseData }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // 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 diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 09c0fe2a07c..43c4b9065a7 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -178,6 +178,8 @@ export class EncryptedMessageHandlerService { await this.messagingService.send("refreshCiphers"); return { status: "success" }; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return { status: "failure" }; } @@ -222,6 +224,8 @@ export class EncryptedMessageHandlerService { await this.messagingService.send("refreshCiphers"); return { status: "success" }; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return { status: "failure" }; } diff --git a/apps/desktop/src/vault/app/accounts/premium.component.ts b/apps/desktop/src/vault/app/accounts/premium.component.ts index 4b547384545..bfdaf084648 100644 --- a/apps/desktop/src/vault/app/accounts/premium.component.ts +++ b/apps/desktop/src/vault/app/accounts/premium.component.ts @@ -9,7 +9,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-premium", @@ -25,6 +25,7 @@ export class PremiumComponent extends BasePremiumComponent { dialogService: DialogService, environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + toastService: ToastService, accountService: AccountService, ) { super( @@ -36,6 +37,7 @@ export class PremiumComponent extends BasePremiumComponent { dialogService, environmentService, billingAccountProfileStateService, + toastService, accountService, ); } diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html index a294ce1f63f..713118aeace 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html @@ -4,6 +4,7 @@ - - + diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index abf74371912..ae6f031005e 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -14,6 +14,7 @@ import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, } from "@bitwarden/generator-components"; +import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; type CredentialGeneratorParams = { @@ -38,12 +39,17 @@ type CredentialGeneratorParams = { }) export class CredentialGeneratorDialogComponent { credentialValue?: string; + buttonLabel?: string; constructor( @Inject(DIALOG_DATA) protected data: CredentialGeneratorParams, private dialogService: DialogService, ) {} + algorithm = (selected: AlgorithmInfo) => { + this.buttonLabel = selected.useGeneratedValue; + }; + applyCredentials = () => { this.data.onCredentialGenerated(this.credentialValue); }; diff --git a/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts b/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts index 1cab5a940dd..cdb879693c0 100644 --- a/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts @@ -8,7 +8,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @Component({ @@ -26,6 +26,7 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { logService: LogService, dialogService: DialogService, formBuilder: FormBuilder, + toastService: ToastService, ) { super( folderService, @@ -37,6 +38,7 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { logService, dialogService, formBuilder, + toastService, ); } } diff --git a/apps/desktop/src/vault/app/vault/password-history.component.ts b/apps/desktop/src/vault/app/vault/password-history.component.ts index 12701ac5527..4a87617d8f4 100644 --- a/apps/desktop/src/vault/app/vault/password-history.component.ts +++ b/apps/desktop/src/vault/app/vault/password-history.component.ts @@ -5,6 +5,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-password-history", @@ -16,7 +17,8 @@ export class PasswordHistoryComponent extends BasePasswordHistoryComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, accountService: AccountService, + toastService: ToastService, ) { - super(cipherService, platformUtilsService, i18nService, accountService, window); + super(cipherService, platformUtilsService, i18nService, accountService, window, toastService); } } diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts index 92c75e30417..39f1c0200ea 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts @@ -7,6 +7,7 @@ import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/displa import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-organization-filter", @@ -25,6 +26,7 @@ export class OrganizationFilterComponent extends BaseOrganizationFilterComponent constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, ) { super(); } @@ -36,11 +38,11 @@ export class OrganizationFilterComponent extends BaseOrganizationFilterComponent // eslint-disable-next-line @typescript-eslint/no-floating-promises super.applyOrganizationFilter(organization); } else { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("disabledOrganizationFilterError"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("disabledOrganizationFilterError"), + }); } } } diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index ec2dbec5b8f..da1f7bb3160 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -10,8 +10,8 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil, switchMap } from "rxjs"; -import { first } from "rxjs/operators"; +import { combineLatest, firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; +import { filter, first, map, take } from "rxjs/operators"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -28,13 +28,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; import { GeneratorComponent } from "../../../app/tools/generator.component"; @@ -111,8 +113,10 @@ export class VaultComponent implements OnInit, OnDestroy { private apiService: ApiService, private dialogService: DialogService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, private configService: ConfigService, private accountService: AccountService, + private cipherService: CipherService, ) {} async ngOnInit() { @@ -238,6 +242,25 @@ export class VaultComponent implements OnInit, OnDestroy { notificationId: authRequest.id, }); } + + // Store a reference to the current active account during page init + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + // Combine with the activeAccount$ to ensure we only show the dialog for the current account from ngOnInit. + // The account switching process updates the cipherService before Vault is destroyed and would cause duplicate emissions + combineLatest([this.accountService.activeAccount$, this.cipherService.failedToDecryptCiphers$]) + .pipe( + filter(([account]) => account.id === activeAccount.id), + map(([_, ciphers]) => ciphers.filter((c) => !c.isDeleted)), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); } ngOnDestroy() { @@ -302,6 +325,12 @@ export class VaultComponent implements OnInit, OnDestroy { }), }, ]; + + if (cipher.decryptionFailure) { + invokeMenu(menu); + return; + } + if (!cipher.isDeleted) { menu.push({ label: this.i18nService.t("edit"), @@ -781,11 +810,11 @@ export class VaultComponent implements OnInit, OnDestroy { } this.platformUtilsService.copyToClipboard(value); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)), + }); if (this.action === "view") { this.messagingService.send("minimizeOnCopy"); } diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index e6c20d2e89e..f589ba53046 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -186,6 +186,16 @@

+
+
+ {{ "verificationCodeTotp" | i18n }} + + {{ "organizationUpgradeRequired" | i18n }} + + +
+
@@ -638,33 +648,35 @@

-

{{ "deviceListDescription" | i18n }}

+

{{ "deviceListDescriptionTemp" | i18n }}

@@ -48,28 +48,43 @@

{{ "devices" | i18n }}

- {{ row.displayName }} - - {{ "trusted" | i18n }} - + + + {{ row.displayName }} + + + + {{ "needsApproval" | i18n }} + + + + {{ row.displayName }} + + {{ "trusted" | i18n }} + +
{{ "currentSession" | i18n }} - {{ + {{ "requestPending" | i18n }} {{ row.firstLogin | date: "medium" }} - + diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 65f2afc250e..e22122ad9ae 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -1,10 +1,14 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { firstValueFrom } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { combineLatest, firstValueFrom } from "rxjs"; +import { LoginApprovalComponent } from "@bitwarden/auth/angular"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; +import { + DevicePendingAuthRequest, + DeviceResponse, +} from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -26,7 +30,8 @@ interface DeviceTableData { loginStatus: string; firstLogin: Date; trusted: boolean; - devicePendingAuthRequest: object | null; + devicePendingAuthRequest: DevicePendingAuthRequest | null; + hasPendingAuthRequest: boolean; } /** @@ -52,28 +57,25 @@ export class DeviceManagementComponent { private toastService: ToastService, private validationService: ValidationService, ) { - this.devicesService - .getCurrentDevice$() - .pipe( - takeUntilDestroyed(), - switchMap((currentDevice) => { - this.currentDevice = new DeviceView(currentDevice); - return this.devicesService.getDevices$(); - }), - ) + combineLatest([this.devicesService.getCurrentDevice$(), this.devicesService.getDevices$()]) + .pipe(takeUntilDestroyed()) .subscribe({ - next: (devices) => { - this.dataSource.data = devices.map((device) => { + next: ([currentDevice, devices]: [DeviceResponse, Array]) => { + this.currentDevice = new DeviceView(currentDevice); + + this.dataSource.data = devices.map((device: DeviceView): DeviceTableData => { return { id: device.id, type: device.type, displayName: this.getHumanReadableDeviceType(device.type), loginStatus: this.getLoginStatus(device), - devicePendingAuthRequest: device.response.devicePendingAuthRequest, firstLogin: new Date(device.creationDate), trusted: device.response.isTrusted, + devicePendingAuthRequest: device.response.devicePendingAuthRequest, + hasPendingAuthRequest: this.hasPendingAuthRequest(device.response), }; }); + this.loading = false; }, error: () => { @@ -176,15 +178,36 @@ export class DeviceManagementComponent { /** * Check if a device has a pending auth request - * @param device - The device + * @param device - The device response * @returns True if the device has a pending auth request, false otherwise */ - protected hasPendingAuthRequest(device: DeviceTableData): boolean { + private hasPendingAuthRequest(device: DeviceResponse): boolean { return ( device.devicePendingAuthRequest !== undefined && device.devicePendingAuthRequest !== null ); } + /** + * Open a dialog to approve or deny a pending auth request for a device + */ + async managePendingAuthRequest(device: DeviceTableData) { + if (device.devicePendingAuthRequest === undefined || device.devicePendingAuthRequest === null) { + return; + } + + const dialogRef = LoginApprovalComponent.open(this.dialogService, { + notificationId: device.devicePendingAuthRequest.id, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result !== undefined && typeof result === "boolean") { + // auth request approved or denied so reset + device.devicePendingAuthRequest = null; + device.hasPendingAuthRequest = false; + } + } + /** * Remove a device * @param device - The device diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 3b20718873d..4530692ebee 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; -import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { first, firstValueFrom, @@ -14,7 +14,6 @@ import { } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -67,11 +66,10 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { constructor( protected dialogService: DialogService, protected apiService: ApiService, - protected modalService: ModalService, protected messagingService: MessagingService, protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, - private accountService: AccountService, + protected accountService: AccountService, ) { this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => @@ -268,13 +266,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { return type === TwoFactorProviderType.OrganizationDuo; } - protected async openModal(ref: ViewContainerRef, type: Type): Promise { - const [modal, childComponent] = await this.modalService.openViewRef(type, ref); - this.modal = modal; - - return childComponent; - } - protected updateStatus(enabled: boolean, type: TwoFactorProviderType) { if (!enabled && this.modal != null) { this.modal.close(); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts index 48a7f40d238..c12d7941c44 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -10,6 +10,8 @@ import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstract import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DialogService } from "@bitwarden/components/src/dialog/dialog.service"; import { WebauthnLoginAdminService } from "../../../core/services/webauthn-login/webauthn-login-admin.service"; diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts index 464c00c4a3a..d49621222f6 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts @@ -10,7 +10,7 @@ import { RegisterFormModule } from "../../auth/register-form/register-form.modul import { SecretsManagerTrialFreeStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component"; import { SecretsManagerTrialPaidStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component"; import { SecretsManagerTrialComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial.component"; -import { PaymentComponent, TaxInfoComponent } from "../../billing"; +import { TaxInfoComponent } from "../../billing"; import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module"; import { SharedModule } from "../../shared"; @@ -51,7 +51,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul RegisterFormModule, OrganizationCreateModule, EnvironmentSelectorModule, - PaymentComponent, TaxInfoComponent, TrialBillingStepComponent, InputPasswordComponent, diff --git a/apps/web/src/app/auth/two-factor-auth-duo.component.ts b/apps/web/src/app/auth/two-factor-auth-duo.component.ts index b82632008bd..971c1f3764c 100644 --- a/apps/web/src/app/auth/two-factor-auth-duo.component.ts +++ b/apps/web/src/app/auth/two-factor-auth-duo.component.ts @@ -8,11 +8,23 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; @Component({ diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index 18660b2ca63..cbe1d8f0a53 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -25,19 +25,39 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthWebAuthnComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthYubikeyComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "../../../../../libs/auth/src/common/abstractions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index 2f983944b70..5bcde6a697a 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -49,15 +49,7 @@

{{ "billingPlanLabel" | i18n }

{{ "paymentType" | i18n }}

- - +
diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 213f051f75e..1c24bb9fd80 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -13,14 +13,12 @@ import { } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ToastService } from "@bitwarden/components"; -import { BillingSharedModule, PaymentComponent, TaxInfoComponent } from "../../shared"; -import { PaymentV2Component } from "../../shared/payment/payment-v2.component"; +import { BillingSharedModule, TaxInfoComponent } from "../../shared"; +import { PaymentComponent } from "../../shared/payment/payment.component"; export type TrialOrganizationType = Exclude; @@ -53,7 +51,6 @@ export enum SubscriptionProduct { }) export class TrialBillingStepComponent implements OnInit { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; @Input() organizationInfo: OrganizationInfo; @Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager; @@ -74,11 +71,8 @@ export class TrialBillingStepComponent implements OnInit { annualPlan?: PlanResponse; monthlyPlan?: PlanResponse; - deprecateStripeSourcesAPI: boolean; - constructor( private apiService: ApiService, - private configService: ConfigService, private i18nService: I18nService, private formBuilder: FormBuilder, private messagingService: MessagingService, @@ -87,9 +81,6 @@ export class TrialBillingStepComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); const plans = await this.apiService.getPlans(); this.applicablePlans = plans.data.filter(this.isApplicable); this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual); @@ -124,23 +115,12 @@ export class TrialBillingStepComponent implements OnInit { } protected changedCountry() { - if (this.deprecateStripeSourcesAPI) { - this.paymentV2Component.showBankAccount = this.taxInfoComponent.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else { - this.paymentComponent.hideBank = this.taxInfoComponent.taxFormGroup.value.country !== "US"; - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + this.paymentComponent.showBankAccount = this.taxInfoComponent.country === "US"; + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -162,14 +142,8 @@ export class TrialBillingStepComponent implements OnInit { private async createOrganization(): Promise { const planResponse = this.findPlanFor(this.formGroup.value.cadence); - let paymentMethod: [string, PaymentMethodType]; - - if (this.deprecateStripeSourcesAPI) { - const { type, token } = await this.paymentV2Component.tokenize(); - paymentMethod = [token, type]; - } else { - paymentMethod = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod: [string, PaymentMethodType] = [token, type]; const organization: OrganizationInformation = { name: this.organizationInfo.name, diff --git a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts index 0f6baa5f322..75054d78db1 100644 --- a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts +++ b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts @@ -1,15 +1,35 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const organizationService = inject(OrganizationService); const providerService = inject(ProviderService); + const accountService = inject(AccountService); - const organization = await organizationService.get(route.params.organizationId); + const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id))); + + if (!userId) { + throw new Error("No user found."); + } + + const organization = await firstValueFrom( + organizationService + .organizations$(userId) + .pipe(getOrganizationById(route.params.organizationId)), + ); + + if (!organization) { + throw new Error("No organization found."); + } if (!organization.hasProvider) { return true; diff --git a/apps/web/src/app/billing/index.ts b/apps/web/src/app/billing/index.ts index b59ab33e54c..217f1e05be9 100644 --- a/apps/web/src/app/billing/index.ts +++ b/apps/web/src/app/billing/index.ts @@ -1,2 +1,2 @@ export { OrganizationPlansComponent } from "./organizations"; -export { PaymentComponent, TaxInfoComponent } from "./shared"; +export { TaxInfoComponent } from "./shared"; diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index 585d9b418c1..bb1ada0b719 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -1,13 +1,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - import { PaymentMethodComponent } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; -import { PremiumV2Component } from "./premium/premium-v2.component"; import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -24,15 +20,11 @@ const routes: Routes = [ component: UserSubscriptionComponent, data: { titleId: "premiumMembership" }, }, - ...featureFlaggedRoute({ - defaultComponent: PremiumComponent, - flaggedComponent: PremiumV2Component, - featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - routeOptions: { - path: "premium", - data: { titleId: "goPremium" }, - }, - }), + { + path: "premium", + component: PremiumComponent, + data: { titleId: "goPremium" }, + }, { path: "payment-method", component: PaymentMethodComponent, diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts index 0dbbc8c6837..ad75da00c99 100644 --- a/apps/web/src/app/billing/individual/individual-billing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing.module.ts @@ -5,7 +5,6 @@ import { BillingSharedModule } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { IndividualBillingRoutingModule } from "./individual-billing-routing.module"; -import { PremiumV2Component } from "./premium/premium-v2.component"; import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -17,7 +16,6 @@ import { UserSubscriptionComponent } from "./user-subscription.component"; BillingHistoryViewComponent, UserSubscriptionComponent, PremiumComponent, - PremiumV2Component, ], }) export class IndividualBillingModule {} diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.html b/apps/web/src/app/billing/individual/premium/premium-v2.component.html deleted file mode 100644 index 7adc93fd962..00000000000 --- a/apps/web/src/app/billing/individual/premium/premium-v2.component.html +++ /dev/null @@ -1,149 +0,0 @@ - -

{{ "goPremium" | i18n }}

- - {{ "alreadyPremiumFromOrg" | i18n }} - - -

{{ "premiumUpgradeUnlockFeatures" | i18n }}

-
    -
  • - - {{ "premiumSignUpStorage" | i18n }} -
  • -
  • - - {{ "premiumSignUpTwoStepOptions" | i18n }} -
  • -
  • - - {{ "premiumSignUpEmergency" | i18n }} -
  • -
  • - - {{ "premiumSignUpReports" | i18n }} -
  • -
  • - - {{ "premiumSignUpTotp" | i18n }} -
  • -
  • - - {{ "premiumSignUpSupport" | i18n }} -
  • -
  • - - {{ "premiumSignUpFuture" | i18n }} -
  • -
-

- {{ - "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount - }} - - {{ "bitwardenFamiliesPlan" | i18n }} - -

- - {{ "purchasePremium" | i18n }} - -
-
- - -

{{ "uploadLicenseFilePremium" | i18n }}

-
- - {{ "licenseFile" | i18n }} -
- - {{ - licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) - }} -
- - {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} -
- -
-
- -
-
- -

{{ "addons" | i18n }}

-
- - {{ "additionalStorageGb" | i18n }} - - {{ - "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) - }} - -
-
- -

{{ "summary" | i18n }}

- {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × - {{ storageGBPrice | currency: "$" }} = - {{ additionalStorageCost | currency: "$" }} -
-
- -

{{ "paymentInformation" | i18n }}

- - -
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} - {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }} -
-
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

- -
-
diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts deleted file mode 100644 index 11b55f92b40..00000000000 --- a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts +++ /dev/null @@ -1,228 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, ViewChild } from "@angular/core"; -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, switchMap } from "rxjs"; -import { debounceTime } from "rxjs/operators"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { ToastService } from "@bitwarden/components"; - -import { PaymentV2Component } from "../../shared/payment/payment-v2.component"; -import { TaxInfoComponent } from "../../shared/tax-info.component"; - -@Component({ - templateUrl: "./premium-v2.component.html", -}) -export class PremiumV2Component { - @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; - @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - - protected hasPremiumFromAnyOrganization$: Observable; - - protected addOnFormGroup = new FormGroup({ - additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), - }); - - protected licenseFormGroup = new FormGroup({ - file: new FormControl(null, [Validators.required]), - }); - - protected cloudWebVaultURL: string; - protected isSelfHost = false; - - protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( - FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, - ); - - protected estimatedTax: number = 0; - protected readonly familyPlanMaxUserCount = 6; - protected readonly premiumPrice = 10; - protected readonly storageGBPrice = 4; - - constructor( - private activatedRoute: ActivatedRoute, - private apiService: ApiService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - private configService: ConfigService, - private environmentService: EnvironmentService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private syncService: SyncService, - private toastService: ToastService, - private tokenService: TokenService, - private taxService: TaxServiceAbstraction, - private accountService: AccountService, - ) { - this.isSelfHost = this.platformUtilsService.isSelfHost(); - - this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), - ), - ); - - combineLatest([ - this.accountService.activeAccount$.pipe( - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), - ), - ), - this.environmentService.cloudWebVaultUrl$, - ]) - .pipe( - takeUntilDestroyed(), - concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { - if (hasPremiumPersonally) { - return from(this.navigateToSubscriptionPage()); - } - - this.cloudWebVaultURL = cloudWebVaultURL; - return of(true); - }), - ) - .subscribe(); - - this.addOnFormGroup.controls.additionalStorage.valueChanges - .pipe(debounceTime(1000), takeUntilDestroyed()) - .subscribe(() => { - this.refreshSalesTax(); - }); - } - - finalizeUpgrade = async () => { - await this.apiService.refreshIdentityToken(); - await this.syncService.fullSync(true); - }; - - postFinalizeUpgrade = async () => { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("premiumUpdated"), - }); - await this.navigateToSubscriptionPage(); - }; - - navigateToSubscriptionPage = (): Promise => - this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); - - onLicenseFileSelected = (event: Event): void => { - const element = event.target as HTMLInputElement; - this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; - }; - - submitPremiumLicense = async (): Promise => { - this.licenseFormGroup.markAllAsTouched(); - - if (this.licenseFormGroup.invalid) { - return this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - } - - const emailVerified = await this.tokenService.getEmailVerified(); - if (!emailVerified) { - return this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - } - - const formData = new FormData(); - formData.append("license", this.licenseFormGroup.value.file); - - await this.apiService.postAccountLicense(formData); - await this.finalizeUpgrade(); - await this.postFinalizeUpgrade(); - }; - - submitPayment = async (): Promise => { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - if (this.taxInfoComponent.taxFormGroup.invalid) { - return; - } - - const { type, token } = await this.paymentComponent.tokenize(); - - const formData = new FormData(); - formData.append("paymentMethodType", type.toString()); - formData.append("paymentToken", token); - formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); - formData.append("country", this.taxInfoComponent.country); - formData.append("postalCode", this.taxInfoComponent.postalCode); - - await this.apiService.postPremium(formData); - await this.finalizeUpgrade(); - await this.postFinalizeUpgrade(); - }; - - protected get additionalStorageCost(): number { - return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; - } - - protected get premiumURL(): string { - return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; - } - - protected get subtotal(): number { - return this.premiumPrice + this.additionalStorageCost; - } - - protected get total(): number { - return this.subtotal + this.estimatedTax; - } - - protected async onLicenseFileSelectedChanged(): Promise { - 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(); - } -} diff --git a/apps/web/src/app/billing/individual/premium/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html index 12b6932d0f5..acf24ed2a34 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.html +++ b/apps/web/src/app/billing/individual/premium/premium.component.html @@ -1,8 +1,8 @@ -

{{ "goPremium" | i18n }}

+

{{ "goPremium" | i18n }}

@@ -40,7 +40,7 @@

{{ "goPremium" | i18n }}

{{ "premiumSignUpFuture" | i18n }} -

+

{{ "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount }} @@ -49,49 +49,58 @@

{{ "goPremium" | i18n }}

linkType="primary" routerLink="/create-organization" [queryParams]="{ plan: 'families' }" - >{{ "bitwardenFamiliesPlan" | i18n }} + {{ "bitwardenFamiliesPlan" | i18n }} +

{{ "purchasePremium" | i18n }}
- -

{{ "uploadLicenseFilePremium" | i18n }}

-
- - {{ "licenseFile" | i18n }} -
- - {{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }} -
- - {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} -
- -
+ + +

{{ "uploadLicenseFilePremium" | i18n }}

+
+ + {{ "licenseFile" | i18n }} +
+ + {{ + licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) + }} +
+ + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} +
+ +
+
+
-
+

{{ "addons" | i18n }}

@@ -106,7 +115,7 @@

{{ "addons" | i18n }}

/> {{ "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n) + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) }}
@@ -114,30 +123,26 @@

{{ "addons" | i18n }}

{{ "summary" | i18n }}

{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = - {{ additionalStorageTotal | currency: "$" }} + {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ storageGBPrice | currency: "$" }} = + {{ additionalStorageCost | currency: "$" }}

{{ "paymentInformation" | i18n }}

- - -
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} -
- - {{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} - + + +
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} + {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

-

{{ "paymentChargedAnnually" | i18n }}

- diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index f96f573cd4d..ec19eb02594 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -1,187 +1,197 @@ // 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 { Component, 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, switchMap } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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"; 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ToastService } from "@bitwarden/components"; -import { PaymentComponent, TaxInfoComponent } from "../../shared"; +import { PaymentComponent } from "../../shared/payment/payment.component"; +import { TaxInfoComponent } from "../../shared/tax-info.component"; @Component({ - templateUrl: "premium.component.html", + templateUrl: "./premium.component.html", }) -export class PremiumComponent implements OnInit { +export class PremiumComponent { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - canAccessPremium$: Observable; - selfHosted = false; - premiumPrice = 10; - familyPlanMaxUserCount = 6; - storageGbPrice = 4; - cloudWebVaultUrl: string; - licenseFile: File = null; - - formPromise: Promise; - protected licenseForm = new FormGroup({ - file: new FormControl(null, [Validators.required]), + protected hasPremiumFromAnyOrganization$: Observable; + + protected addOnFormGroup = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), }); - protected addonForm = new FormGroup({ - additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]), + + protected licenseFormGroup = new FormGroup({ + file: new FormControl(null, [Validators.required]), }); - private estimatedTax: number = 0; + protected cloudWebVaultURL: string; + protected isSelfHost = false; + + protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( + FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, + ); + + protected estimatedTax: number = 0; + protected readonly familyPlanMaxUserCount = 6; + protected readonly premiumPrice = 10; + protected readonly storageGBPrice = 4; constructor( + private activatedRoute: ActivatedRoute, private apiService: ApiService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private configService: ConfigService, + private environmentService: EnvironmentService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private tokenService: TokenService, private router: Router, - private messagingService: MessagingService, private syncService: SyncService, - private environmentService: EnvironmentService, - private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, + private tokenService: TokenService, private taxService: TaxServiceAbstraction, private accountService: AccountService, ) { - this.selfHosted = platformUtilsService.isSelfHost(); - this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + this.isSelfHost = this.platformUtilsService.isSelfHost(); + + this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), ), ); - this.addonForm.controls.additionalStorage.valueChanges + combineLatest([ + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), + ), + ), + this.environmentService.cloudWebVaultUrl$, + ]) + .pipe( + takeUntilDestroyed(), + concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + + this.cloudWebVaultURL = cloudWebVaultURL; + return of(true); + }), + ) + .subscribe(); + + this.addOnFormGroup.controls.additionalStorage.valueChanges .pipe(debounceTime(1000), takeUntilDestroyed()) .subscribe(() => { this.refreshSalesTax(); }); } - protected setSelectedFile(event: Event) { - const fileInputEl = event.target; - const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; - this.licenseFile = file; - } - async ngOnInit() { - this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - const account = await firstValueFrom(this.accountService.activeAccount$); - if ( - await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)) - ) { - // 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.router.navigate(["/settings/subscription/user-subscription"]); - return; - } - } - submit = async () => { - if (this.taxInfoComponent) { - if (!this.taxInfoComponent?.taxFormGroup.valid) { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - return; - } - } - this.licenseForm.markAllAsTouched(); - this.addonForm.markAllAsTouched(); - if (this.selfHosted) { - if (this.licenseFile == null) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - return; - } - } - - if (this.selfHosted) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (!this.tokenService.getEmailVerified()) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - return; - } - - const fd = new FormData(); - fd.append("license", this.licenseFile); - await this.apiService.postAccountLicense(fd).then(() => { - return this.finalizePremium(); - }); - } else { - await this.paymentComponent - .createPaymentToken() - .then((result) => { - const fd = new FormData(); - fd.append("paymentMethodType", result[1].toString()); - if (result[0] != null) { - fd.append("paymentToken", result[0]); - } - fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); - fd.append("country", this.taxInfoComponent?.taxFormGroup?.value.country); - fd.append("postalCode", this.taxInfoComponent?.taxFormGroup?.value.postalCode); - return this.apiService.postPremium(fd); - }) - .then((paymentResponse) => { - if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { - return this.paymentComponent.handleStripeCardPayment( - paymentResponse.paymentIntentClientSecret, - () => this.finalizePremium(), - ); - } else { - return this.finalizePremium(); - } - }); - } - }; - async finalizePremium() { + finalizeUpgrade = async () => { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); + }; + + postFinalizeUpgrade = async () => { this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("premiumUpdated"), }); - await this.router.navigate(["/settings/subscription/user-subscription"]); - } + await this.navigateToSubscriptionPage(); + }; + + navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + onLicenseFileSelected = (event: Event): void => { + const element = event.target as HTMLInputElement; + this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; + }; + + submitPremiumLicense = async (): Promise => { + this.licenseFormGroup.markAllAsTouched(); + + if (this.licenseFormGroup.invalid) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + } + + const emailVerified = await this.tokenService.getEmailVerified(); + if (!emailVerified) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); + } - get additionalStorage(): number { - return this.addonForm.get("additionalStorage").value; + const formData = new FormData(); + formData.append("license", this.licenseFormGroup.value.file); + + await this.apiService.postAccountLicense(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + submitPayment = async (): Promise => { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const { type, token } = await this.paymentComponent.tokenize(); + + const formData = new FormData(); + formData.append("paymentMethodType", type.toString()); + formData.append("paymentToken", token); + formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); + formData.append("country", this.taxInfoComponent.country); + formData.append("postalCode", this.taxInfoComponent.postalCode); + + await this.apiService.postPremium(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + protected get additionalStorageCost(): number { + return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; } - get additionalStorageTotal(): number { - return this.storageGbPrice * Math.abs(this.additionalStorage || 0); + + protected get premiumURL(): string { + return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; } - get subtotal(): number { - return this.premiumPrice + this.additionalStorageTotal; + protected get subtotal(): number { + return this.premiumPrice + this.additionalStorageCost; } - get taxCharges(): number { - return this.estimatedTax; + protected get total(): number { + return this.subtotal + this.estimatedTax; } - get total(): number { - return this.subtotal + this.taxCharges || 0; + protected async onLicenseFileSelectedChanged(): Promise { + await this.postFinalizeUpgrade(); } private refreshSalesTax(): void { @@ -190,7 +200,7 @@ export class PremiumComponent implements OnInit { } const request: PreviewIndividualInvoiceRequest = { passwordManager: { - additionalStorage: this.addonForm.value.additionalStorage, + additionalStorage: this.addOnFormGroup.value.additionalStorage, }, taxInformation: { postalCode: this.taxInfoComponent.postalCode, diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 97b4725e6d7..38f4436fb47 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -8,8 +8,6 @@ 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 { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; -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"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -18,12 +16,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -45,10 +39,6 @@ export class UserSubscriptionComponent implements OnInit { cancelPromise: Promise; reinstatePromise: Promise; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -60,7 +50,6 @@ export class UserSubscriptionComponent implements OnInit { private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, - private configService: ConfigService, private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); @@ -166,33 +155,18 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); - - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: 4, - cadence: "year", - type: add ? "Add" : "Remove", - }, - }); + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); - const result = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 0166560007e..786c25c8d4c 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, 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 { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; 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"; @@ -37,6 +42,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { private formBuilder: FormBuilder, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -73,14 +79,19 @@ export class AdjustSubscription implements OnInit, OnDestroy { request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 902cac9c771..b5471a90fd5 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -30,7 +30,7 @@ }} -
+
@@ -346,25 +346,20 @@

{{ "paymentMethod" | i18n }}

" > - {{ - deprecateStripeSourcesAPI - ? paymentSource?.description - : billing?.paymentSource?.description - }} + {{ paymentSource?.description }} {{ "changePaymentMethod" | i18n }}

- - + -
+

{{ "total" | i18n }}: @@ -400,7 +395,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.PasswordManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -427,7 +422,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
@@ -444,7 +439,7 @@

{{ "paymentMethod" | i18n }}

{{ "additionalStorageGbMessage" | i18n }} × {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ additionalStorageTotal(selectedPlan) | currency: "$" }}

@@ -485,7 +480,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.SecretsManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
@@ -524,7 +519,7 @@

{{ "paymentMethod" | i18n }}

{{ "serviceAccounts" | i18n | lowercase }} × {{ selectedPlan?.SecretsManager?.additionalPricePerServiceAccount | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

@@ -580,7 +575,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }} {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} @@ -596,7 +591,7 @@

{{ "paymentMethod" | i18n }}

{{ "additionalStorageGbMessage" | i18n }} × {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ storageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" @@ -656,7 +651,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} @@ -675,7 +670,7 @@

{{ "paymentMethod" | i18n }}

{{ "serviceAccounts" | i18n | lowercase }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

@@ -725,7 +720,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.SecretsManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -764,7 +759,7 @@

{{ "paymentMethod" | i18n }}

{{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

@@ -786,7 +781,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.PasswordManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -813,7 +808,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
@@ -860,7 +855,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} @@ -879,7 +874,7 @@

{{ "paymentMethod" | i18n }}

{{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

@@ -914,7 +909,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }} {{ "freeForOneYear" | i18n }} @@ -962,7 +957,7 @@

{{ "paymentMethod" | i18n }}

-
+

; isSubscriptionCanceled: boolean = false; private destroy$ = new Subject(); @@ -205,17 +203,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, private organizationBillingService: OrganizationBillingService, ) {} async ngOnInit(): Promise { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.dialogParams.organizationId) { this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); this.sub = @@ -225,21 +219,24 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.organizationId = this.dialogParams.organizationId; this.currentPlan = this.sub?.plan; this.selectedPlan = this.sub?.plan; - this.organization = await this.organizationService.get(this.organizationId); - if (this.deprecateStripeSourcesAPI) { - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - } else { - this.billing = await this.organizationApiService.getBilling(this.organizationId); - } + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + const { accountCredit, paymentSource } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; } if (!this.selfHosted) { - const plans = await this.apiService.getPlans(); - this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager); - this.secretsManagerPlans = plans.data.filter((plan) => !!plan.SecretsManager); + this.plans = await this.apiService.getPlans(); + this.passwordManagerPlans = this.plans.data.filter((plan) => !!plan.PasswordManager); + this.secretsManagerPlans = this.plans.data.filter((plan) => !!plan.SecretsManager); if ( this.productTier === ProductTierType.Enterprise || @@ -311,23 +308,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { setInitialPlanSelection() { this.focusedIndex = this.selectableProducts.length - 1; - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + if (!this.isSubscriptionCanceled) { + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + } } getPlanByType(productTier: ProductTierType) { return this.selectableProducts.find((product) => product.productTier === productTier); } - secretsManagerTrialDiscount() { - return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") - ? this.discountPercentage - : this.discountPercentageFromSub + this.discountPercentage; - } - isPaymentSourceEmpty() { - return this.deprecateStripeSourcesAPI - ? this.paymentSource === null || this.paymentSource === undefined - : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + return this.paymentSource === null || this.paymentSource === undefined; } isSecretsManagerTrial(): boolean { @@ -471,9 +462,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get upgradeRequiresPaymentMethod() { const isFreeTier = this.organization?.productTierType === ProductTierType.Free; const shouldHideFree = !this.showFree; - const hasNoPaymentSource = this.deprecateStripeSourcesAPI - ? !this.paymentSource - : !this.billing?.paymentSource; + const hasNoPaymentSource = !this.paymentSource; return isFreeTier && shouldHideFree && hasNoPaymentSource; } @@ -483,6 +472,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get selectedPlanInterval() { + if (this.isSubscriptionCanceled) { + return this.currentPlan.isAnnual ? "year" : "month"; + } return this.selectedPlan.isAnnual ? "year" : "month"; } @@ -703,25 +695,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } changedCountry() { - if (this.deprecateStripeSourcesAPI && this.paymentV2Component) { - this.paymentV2Component.showBankAccount = this.taxInformation.country === "US"; + this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } 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 && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -786,8 +766,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { billingEmail: org.billingEmail, }; + const filteredPlan = this.plans.data + .filter((plan) => plan.productTier === this.selectedPlan.productTier && !plan.legacyYear) + .find((plan) => { + const isSameBillingCycle = plan.isAnnual === this.selectedPlan.isAnnual; + return isSameBillingCycle; + }); + const plan: PlanInformation = { - type: this.selectedPlan.type, + type: filteredPlan.type, passwordManagerSeats: org.seats, }; @@ -796,14 +783,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { plan.secretsManagerSeats = org.smSeats; } - let paymentMethod: [string, PaymentMethodType]; - - if (this.deprecateStripeSourcesAPI) { - const { type, token } = await this.paymentV2Component.tokenize(); - paymentMethod = [token, type]; - } else { - paymentMethod = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod: [string, PaymentMethodType] = [token, type]; const payment: PaymentInformation = { paymentMethod, @@ -839,27 +820,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { - if (this.deprecateStripeSourcesAPI) { - const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const tokenResult = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = tokenResult[0]; - paymentRequest.paymentMethodType = tokenResult[1]; - paymentRequest.country = this.taxInformation.country; - paymentRequest.postalCode = this.taxInformation.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const tokenizedPaymentSource = await this.paymentComponent.tokenize(); + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -869,10 +840,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - const result = await this.organizationApiService.upgrade(this.organizationId, request); - if (!result.success && result.paymentIntentClientSecret != null) { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -969,38 +937,20 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get paymentSourceClasses() { - if (this.deprecateStripeSourcesAPI) { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - return ["bwi-bank"]; - case PaymentMethodType.Check: - return ["bwi-money"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } else { - if (this.billing.paymentSource == null) { + if (this.paymentSource == null) { + return []; + } + switch (this.paymentSource.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.Check: + return ["bwi-money"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal text-primary"]; + default: return []; - } - switch (this.billing.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - return ["bwi-bank"]; - case PaymentMethodType.Check: - return ["bwi-money"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } } } @@ -1060,7 +1010,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } private refreshSalesTax(): void { - if (!this.taxInformation.country || !this.taxInformation.postalCode) { + if ( + this.taxInformation === undefined || + !this.taxInformation.country || + !this.taxInformation.postalCode + ) { return; } diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 3d4c8dd3870..1bfb9fc4912 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -1,14 +1,11 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessBillingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationIsUnmanaged } from "../../billing/guards/organization-is-unmanaged.guard"; import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; -import { PaymentMethodComponent } from "../shared"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; @@ -28,21 +25,17 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - ...featureFlaggedRoute({ - defaultComponent: PaymentMethodComponent, - flaggedComponent: OrganizationPaymentMethodComponent, - featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - routeOptions: { - path: "payment-method", - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", - }, + { + path: "payment-method", + component: OrganizationPaymentMethodComponent, + canActivate: [ + organizationPermissionsGuard((org) => org.canEditPaymentMethods), + organizationIsUnmanaged, + ], + data: { + titleId: "paymentMethod", }, - }), + }, { path: "history", component: OrgBillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 48ac613711d..d8f4b7393aa 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index d37f95e3aa2..2566250c823 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -433,13 +433,7 @@

{{ paymentDesc }}

- - + }}

- - - diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 4592f8de894..f1e090ef50d 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -11,13 +11,16 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { debounceTime, map } 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -27,12 +30,12 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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"; @@ -53,7 +56,6 @@ import { KeyService } from "@bitwarden/key-management"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentV2Component } from "../shared/payment/payment-v2.component"; import { PaymentComponent } from "../shared/payment/payment.component"; interface OnSuccessArgs { @@ -75,7 +77,6 @@ const Allowed2020PlansForLegacyProviders = [ }) export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; @Input() organizationId?: string; @@ -124,7 +125,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; - deprecateStripeSourcesAPI: boolean; protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, @@ -179,21 +179,25 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } async ngOnInit() { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.organizationId) { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(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 { + } else if (!this.selfHosted) { this.taxInformation = await this.apiService.getTaxInfo(); } @@ -568,23 +572,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } protected changedCountry(): void { - if (this.deprecateStripeSourcesAPI) { - this.paymentV2Component.showBankAccount = this.taxInformation?.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else { - this.paymentComponent.hideBank = this.taxInformation?.country !== "US"; - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + this.paymentComponent.showBankAccount = this.taxInformation?.country === "US"; + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -605,6 +598,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { submit = async () => { if (this.taxComponent && !this.taxComponent.validate()) { + this.taxComponent.markAllAsTouched(); return; } @@ -738,25 +732,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - if (this.deprecateStripeSourcesAPI) { - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = await this.paymentV2Component.tokenize(); - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const [paymentToken, paymentMethodType] = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = paymentToken; - paymentRequest.paymentMethodType = paymentMethodType; - paymentRequest.country = this.taxInformation?.country; - paymentRequest.postalCode = this.taxInformation?.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize(); + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -766,10 +750,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - const result = await this.organizationApiService.upgrade(this.organizationId, request); - if (!result.success && result.paymentIntentClientSecret != null) { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -790,14 +771,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - let type: PaymentMethodType; - let token: string; - - if (this.deprecateStripeSourcesAPI) { - ({ type, token } = await this.paymentV2Component.tokenize()); - } else { - [token, type] = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); request.paymentToken = token; request.paymentMethodType = type; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 09a4890549b..003f816ac30 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -2,13 +2,18 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs"; 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationApiKeyType } 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -20,12 +25,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -42,12 +43,14 @@ import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.comp templateUrl: "organization-subscription-cloud.component.html", }) export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy { + static readonly QUERY_PARAM_UPGRADE: string = "upgrade"; + static readonly ROUTE_PARAM_ORGANIZATION_ID: string = "organizationId"; + sub: OrganizationSubscriptionResponse; lineItems: BillingSubscriptionItemResponse[] = []; organizationId: string; userOrg: Organization; showChangePlan = false; - showDownloadLicense = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -62,10 +65,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - private destroy$ = new Subject(); constructor( @@ -73,6 +72,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private i18nService: I18nService, private logService: LogService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, @@ -82,7 +82,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy ) {} async ngOnInit() { - if (this.route.snapshot.queryParamMap.get("upgrade")) { + this.organizationId = + this.route.snapshot.params[ + OrganizationSubscriptionCloudComponent.ROUTE_PARAM_ORGANIZATION_ID + ]; + await this.load(); + + this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( + FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, + ); + + if ( + this.route.snapshot.queryParams[OrganizationSubscriptionCloudComponent.QUERY_PARAM_UPGRADE] + ) { await this.changePlan(); const productTierTypeStr = this.route.snapshot.queryParamMap.get("productTierType"); if (productTierTypeStr != null) { @@ -92,20 +104,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } } } - - this.route.params - .pipe( - concatMap(async (params) => { - this.organizationId = params.organizationId; - await this.load(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( - FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, - ); } ngOnDestroy() { @@ -116,7 +114,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy async load() { this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner; const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner; @@ -414,36 +417,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorage = (add: boolean) => { return async () => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); - - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: this.storageGbPrice, - cadence: this.billingInterval, - type: add ? "Add" : "Remove", - organizationId: this.organizationId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: this.storageGbPrice, - add: add, - organizationId: this.organizationId, - interval: this.billingInterval, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: this.storageGbPrice, + cadence: this.billingInterval, + type: add ? "Add" : "Remove", + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; }; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index ef68de39526..e6854a5216b 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -7,10 +7,15 @@ import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationConnectionType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingSyncConfigApi } from "@bitwarden/common/billing/models/api/billing-sync-config.api"; import { SelfHostedOrganizationSubscriptionView } from "@bitwarden/common/billing/models/view/self-hosted-organization-subscription.view"; @@ -80,6 +85,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest private messagingService: MessagingService, private apiService: ApiService, private organizationService: OrganizationService, + private accountService: AccountService, private route: ActivatedRoute, private organizationApiService: OrganizationApiServiceAbstraction, private platformUtilsService: PlatformUtilsService, @@ -115,7 +121,12 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest return; } this.loading = true; - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.showAutomaticSyncAndManualUpload = this.userOrg.productTierType == ProductTierType.Families ? false : true; if (this.userOrg.canViewSubscription) { diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 270ba54f70d..3fb2121b036 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -4,11 +4,15 @@ import { Location } from "@angular/common"; 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"; +import { firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs"; 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; @@ -26,9 +30,9 @@ import { openAddCreditDialog, } from "../../shared/add-credit-dialog.component"; import { - AdjustPaymentDialogV2Component, - AdjustPaymentDialogV2ResultType, -} from "../../shared/adjust-payment-dialog/adjust-payment-dialog-v2.component"; + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, +} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ templateUrl: "./organization-payment-method.component.html", @@ -60,6 +64,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { private location: Location, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { this.activatedRoute.params @@ -120,7 +125,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ organizationSubscriptionPromise, @@ -147,7 +159,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }; protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -157,13 +169,13 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { await this.load(); } }; changePayment = async () => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -171,7 +183,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index fc7a188f967..c10c9abc9b6 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, 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 { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; 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"; @@ -107,6 +112,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private platformUtilsService: PlatformUtilsService, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -165,14 +171,19 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 7ad0895809c..617b68abb37 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -2,12 +2,15 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request"; import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; @@ -37,6 +40,7 @@ export class SecretsManagerSubscribeStandaloneComponent { private organizationApiService: OrganizationApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, private toastService: ToastService, + private accountService: AccountService, ) {} submit = async () => { @@ -56,7 +60,8 @@ export class SecretsManagerSubscribeStandaloneComponent { isMember: this.organization.isMember, isProviderUser: this.organization.isProviderUser, }); - await this.organizationService.upsert(organizationData); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + await this.organizationService.upsert(organizationData, userId); /* Because subscribing to Secrets Manager automatically provides access to Secrets Manager for the diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index cc53e0a32bc..b07ccefdbab 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -5,6 +5,8 @@ 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -25,15 +27,34 @@ export class FreeFamiliesPolicyService { constructor( private policyService: PolicyService, private organizationService: OrganizationService, + private accountService: AccountService, private configService: ConfigService, ) {} + canManageSponsorships$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.canManageSponsorships$(account?.id); + } else { + return of(); + } + }), + ); + + organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.organizations$(account?.id); + } else { + return of(); + } + }), + ); + get showFreeFamilies$(): Observable { return this.isFreeFamilyFlagEnabled$.pipe( switchMap((isFreeFamilyFlagEnabled) => - isFreeFamilyFlagEnabled - ? this.getFreeFamiliesVisibility$() - : this.organizationService.canManageSponsorships$, + isFreeFamilyFlagEnabled ? this.getFreeFamiliesVisibility$() : this.canManageSponsorships$, ), ); } @@ -41,7 +62,7 @@ export class FreeFamiliesPolicyService { private getFreeFamiliesVisibility$(): Observable { return combineLatest([ this.checkEnterpriseOrganizationsAndFetchPolicy(), - this.organizationService.canManageSponsorships$, + this.canManageSponsorships$, ]).pipe( map(([orgStatus, canManageSponsorships]) => this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships), @@ -61,7 +82,7 @@ export class FreeFamiliesPolicyService { } checkEnterpriseOrganizationsAndFetchPolicy(): Observable { - return this.organizationService.organizations$.pipe( + return this.organizations$.pipe( filter((organizations) => Array.isArray(organizations) && organizations.length > 0), switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)), ); @@ -90,7 +111,11 @@ export class FreeFamiliesPolicyService { }); } - return this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy).pipe( + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), map((policies) => ({ isFreeFamilyPolicyEnabled: policies.some( (policy) => policy.organizationId === organizationId && policy.enabled, diff --git a/apps/web/src/app/billing/services/reseller-warning.service.ts b/apps/web/src/app/billing/services/reseller-warning.service.ts index bfd5be3233a..2c59ebafe05 100644 --- a/apps/web/src/app/billing/services/reseller-warning.service.ts +++ b/apps/web/src/app/billing/services/reseller-warning.service.ts @@ -33,7 +33,7 @@ export class ResellerWarningService { return { type: "warning", message: this.i18nService.t( - "resellerPastDueWarning", + "resellerPastDueWarningMsg", organization.providerName, this.formatDate(gracePeriodEnd), ), @@ -50,7 +50,7 @@ export class ResellerWarningService { return { type: "info", message: this.i18nService.t( - "resellerOpenInvoiceWarning", + "resellerOpenInvoiceWarningMgs", organization.providerName, this.formatDate(organizationBillingMetadata.invoiceCreatedDate), this.formatDate(organizationBillingMetadata.invoiceDueDate), @@ -68,7 +68,7 @@ export class ResellerWarningService { return { type: "info", message: this.i18nService.t( - "resellerRenewalWarning", + "resellerRenewalWarningMsg", organization.providerName, this.formatDate(organizationBillingMetadata.subPeriodEndDate), ), diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 5e26e80a30a..d4a93ba7cd8 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -19,6 +19,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanSponsorshipType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -90,12 +91,14 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { FeatureFlag.DisableFreeFamiliesSponsorship, ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (this.isFreeFamilyFlagEnabled) { await this.preventAccessToFreeFamiliesPage(); this.availableSponsorshipOrgs$ = combineLatest([ - this.organizationService.organizations$, - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy), + this.organizationService.organizations$(userId), + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), ]).pipe( map(([organizations, policies]) => organizations @@ -111,9 +114,9 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { ), ); } else { - this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)), - ); + this.availableSponsorshipOrgs$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable))); } this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { @@ -126,9 +129,9 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); - this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)), - ); + this.activeSponsorshipOrgs$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))); this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index b40902112c8..6e9e00b0ee1 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -2,12 +2,14 @@ // @ts-strict-ignore import { formatDate } from "@angular/common"; import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -38,6 +40,7 @@ export class SponsoringOrgRowComponent implements OnInit { private toastService: ToastService, private configService: ConfigService, private policyService: PolicyService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -54,17 +57,19 @@ export class SponsoringOrgRowComponent implements OnInit { ); if (this.isFreeFamilyFlagEnabled) { - this.isFreeFamilyPolicyEnabled$ = this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - Array.isArray(policies) && - policies.some( - (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, - ), - ), - ); + this.isFreeFamilyPolicyEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), + map( + (policies) => + Array.isArray(policies) && + policies.some( + (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, + ), + ), + ); } } diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index 71afde81ee3..7860d456685 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -6,7 +6,10 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; @@ -77,7 +80,14 @@ export class AddCreditDialogComponent implements OnInit { this.creditAmount = "20.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; - const org = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); if (org != null) { this.subject = org.name; this.name = org.name; diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html deleted file mode 100644 index bb06f87ca03..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts deleted file mode 100644 index 0a72b8302bc..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts +++ /dev/null @@ -1,177 +0,0 @@ -// 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, 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, 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 { PaymentV2Component } from "../payment/payment-v2.component"; - -export interface AdjustPaymentDialogV2Params { - initialPaymentMethod?: PaymentMethodType; - organizationId?: string; - productTier?: ProductTierType; -} - -export enum AdjustPaymentDialogV2ResultType { - Closed = "closed", - Submitted = "submitted", -} - -@Component({ - templateUrl: "./adjust-payment-dialog-v2.component.html", -}) -export class AdjustPaymentDialogV2Component implements OnInit { - @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; - @ViewChild(forwardRef(() => ManageTaxInformationComponent)) - taxInfoComponent: ManageTaxInformationComponent; - - protected readonly PaymentMethodType = PaymentMethodType; - protected readonly ResultType = AdjustPaymentDialogV2ResultType; - - 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, - private i18nService: I18nService, - private toastService: ToastService, - ) { - const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; - this.dialogHeader = this.i18nService.t(key); - this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; - this.organizationId = this.dialogParams.organizationId; - this.productTier = this.dialogParams.productTier; - } - - 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; - if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - } - - submit = async (): Promise => { - if (!this.taxInfoComponent.validate()) { - return; - } - - 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, - }); - } - }; - - private updateOrganizationPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - 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.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); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustPaymentDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html index de607314354..4f7990f11a3 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html @@ -1,30 +1,29 @@ - - - - - - - - - - - - + + + + + + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index bbae5099afa..0fc49b2ddc1 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -1,59 +1,66 @@ // 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, OnInit, ViewChild } from "@angular/core"; -import { FormGroup } from "@angular/forms"; +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 { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +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 { 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"; -export interface AdjustPaymentDialogData { - organizationId: string; - currentType: PaymentMethodType; +export interface AdjustPaymentDialogParams { + initialPaymentMethod?: PaymentMethodType; + organizationId?: string; + productTier?: ProductTierType; } -export enum AdjustPaymentDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustPaymentDialogResultType { + Closed = "closed", + Submitted = "submitted", } @Component({ - templateUrl: "adjust-payment-dialog.component.html", + templateUrl: "./adjust-payment-dialog.component.html", }) export class AdjustPaymentDialogComponent implements OnInit { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; + @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; + @ViewChild(forwardRef(() => ManageTaxInformationComponent)) + taxInfoComponent: ManageTaxInformationComponent; - organizationId: string; - currentType: PaymentMethodType; - paymentMethodType = PaymentMethodType; + protected readonly PaymentMethodType = PaymentMethodType; + protected readonly ResultType = AdjustPaymentDialogResultType; - protected DialogResult = AdjustPaymentDialogResult; - protected formGroup = new FormGroup({}); + protected dialogHeader: string; + protected initialPaymentMethod: PaymentMethodType; + protected organizationId?: string; + protected productTier?: ProductTierType; protected taxInformation: TaxInformation; constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, private apiService: ApiService, - private i18nService: I18nService, + private billingApiService: BillingApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, private toastService: ToastService, ) { - this.organizationId = data.organizationId; - this.currentType = data.currentType; + const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; + this.dialogHeader = this.i18nService.t(key); + this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; + this.organizationId = this.dialogParams.organizationId; + this.productTier = this.dialogParams.productTier; } ngOnInit(): void { @@ -78,65 +85,92 @@ export class AdjustPaymentDialogComponent implements OnInit { } } - submit = async () => { - 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.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.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); - } - }); - await response; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); - }; - taxInformationChanged(event: TaxInformation) { this.taxInformation = event; if (event.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; + this.paymentComponent.showBankAccount = !!this.organizationId; } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); + this.paymentComponent.showBankAccount = false; + if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { + this.paymentComponent.select(PaymentMethodType.Card); } } } + submit = async (): Promise => { + if (!this.taxInfoComponent.validate()) { + this.taxInfoComponent.markAllAsTouched(); + return; + } + + 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(AdjustPaymentDialogResultType.Submitted); + } catch (error) { + const msg = typeof error == "object" ? error.message : error; + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t(msg) || msg, + }); + } + }; + + private updateOrganizationPaymentMethod = async () => { + const paymentSource = await this.paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); + + await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); + }; + protected get showTaxIdField(): boolean { - return !!this.organizationId; + if (!this.organizationId) { + return false; + } + + switch (this.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } } -} -/** - * Strongly typed helper to open a AdjustPaymentDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustPaymentDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustPaymentDialogComponent, config); + private updatePremiumUserPaymentMethod = async () => { + const { type, token } = await this.paymentComponent.tokenize(); + + const request = new PaymentRequest(); + request.paymentMethodType = type; + request.paymentToken = token; + 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); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustPaymentDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html deleted file mode 100644 index 7b74379acb6..00000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
- - -

{{ body }}

-
- - {{ storageFieldLabel }} - - - - {{ "total" | i18n }} - {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = - {{ this.price * this.formGroup.value.storage | currency: "$" }} / - {{ this.cadence | i18n }} - - -
-
- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts deleted file mode 100644 index ba7619729bf..00000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -// 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 } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -export interface AdjustStorageDialogV2Params { - price: number; - cadence: "month" | "year"; - type: "Add" | "Remove"; - organizationId?: string; -} - -export enum AdjustStorageDialogV2ResultType { - Submitted = "submitted", - Closed = "closed", -} - -@Component({ - templateUrl: "./adjust-storage-dialog-v2.component.html", -}) -export class AdjustStorageDialogV2Component { - protected formGroup = new FormGroup({ - storage: new FormControl(0, [ - Validators.required, - Validators.min(0), - Validators.max(99), - ]), - }); - - protected organizationId?: string; - protected price: number; - protected cadence: "month" | "year"; - - protected title: string; - protected body: string; - protected storageFieldLabel: string; - - protected ResultType = AdjustStorageDialogV2ResultType; - - constructor( - private apiService: ApiService, - @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private organizationApiService: OrganizationApiServiceAbstraction, - private toastService: ToastService, - ) { - this.price = this.dialogParams.price; - this.cadence = this.dialogParams.cadence; - this.organizationId = this.dialogParams.organizationId; - switch (this.dialogParams.type) { - case "Add": - this.title = this.i18nService.t("addStorage"); - this.body = this.i18nService.t("storageAddNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); - break; - case "Remove": - this.title = this.i18nService.t("removeStorage"); - this.body = this.i18nService.t("storageRemoveNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); - break; - } - } - - submit = async () => { - const request = new StorageRequest(); - switch (this.dialogParams.type) { - case "Add": - request.storageGbAdjustment = this.formGroup.value.storage; - break; - case "Remove": - request.storageGbAdjustment = this.formGroup.value.storage * -1; - break; - } - - if (this.organizationId) { - await this.organizationApiService.updateStorage(this.organizationId, request); - } else { - await this.apiService.postAccountStorage(request); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); - - this.dialogRef.close(this.ResultType.Submitted); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustStorageDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html index a597a3ae5ea..832356477c4 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html @@ -1,17 +1,17 @@
- + -

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+

{{ body }}

- {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} - - - {{ "total" | i18n }}: - {{ formGroup.get("storageAdjustment").value || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} + {{ storageFieldLabel }} + + + + {{ "total" | i18n }} + {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = + {{ this.price * this.formGroup.value.storage | currency: "$" }} / + {{ this.cadence | i18n }}
@@ -25,11 +25,10 @@ bitButton bitFormButton buttonType="secondary" - [bitDialogClose]="DialogResult.Cancelled" + [bitDialogClose]="ResultType.Closed" > {{ "cancel" | i18n }}
- diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index f69f9e3eaad..4362e36f857 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -1,132 +1,103 @@ // 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 } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentResponse } from "@bitwarden/common/billing/models/response/payment.response"; import { StorageRequest } from "@bitwarden/common/models/request/storage.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 { DialogService, ToastService } from "@bitwarden/components"; -import { PaymentComponent } from "../payment/payment.component"; - -export interface AdjustStorageDialogData { - storageGbPrice: number; - add: boolean; +export interface AdjustStorageDialogParams { + price: number; + cadence: "month" | "year"; + type: "Add" | "Remove"; organizationId?: string; - interval?: string; } -export enum AdjustStorageDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustStorageDialogResultType { + Submitted = "submitted", + Closed = "closed", } @Component({ - templateUrl: "adjust-storage-dialog.component.html", + templateUrl: "./adjust-storage-dialog.component.html", }) export class AdjustStorageDialogComponent { - storageGbPrice: number; - add: boolean; - organizationId: string; - interval: string; - - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - - protected DialogResult = AdjustStorageDialogResult; protected formGroup = new FormGroup({ - storageAdjustment: new FormControl(0, [ + storage: new FormControl(0, [ Validators.required, Validators.min(0), Validators.max(99), ]), }); + protected organizationId?: string; + protected price: number; + protected cadence: "month" | "year"; + + protected title: string; + protected body: string; + protected storageFieldLabel: string; + + protected ResultType = AdjustStorageDialogResultType; + constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogParams, + private dialogRef: DialogRef, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private activatedRoute: ActivatedRoute, - private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, private toastService: ToastService, ) { - this.storageGbPrice = data.storageGbPrice; - this.add = data.add; - this.organizationId = data.organizationId; - this.interval = data.interval || "year"; + this.price = this.dialogParams.price; + this.cadence = this.dialogParams.cadence; + this.organizationId = this.dialogParams.organizationId; + switch (this.dialogParams.type) { + case "Add": + this.title = this.i18nService.t("addStorage"); + this.body = this.i18nService.t("storageAddNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); + break; + case "Remove": + this.title = this.i18nService.t("removeStorage"); + this.body = this.i18nService.t("storageRemoveNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); + break; + } } submit = async () => { const request = new StorageRequest(); - request.storageGbAdjustment = this.formGroup.value.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; + switch (this.dialogParams.type) { + case "Add": + request.storageGbAdjustment = this.formGroup.value.storage; + break; + case "Remove": + request.storageGbAdjustment = this.formGroup.value.storage * -1; + break; } - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.apiService.postAccountStorage(request); - } else { - response = this.organizationApiService.updateStorage(this.organizationId, request); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - await action(); - this.dialogRef.close(AdjustStorageDialogResult.Adjusted); - if (paymentFailed) { - this.toastService.showToast({ - variant: "warning", - title: null, - message: this.i18nService.t("couldNotChargeCardPayInvoice"), - timeout: 10000, - }); - // 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.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); + if (this.organizationId) { + await this.organizationApiService.updateStorage(this.organizationId, request); } else { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); + await this.apiService.postAccountStorage(request); } - }; - get adjustedStorageTotal(): number { - return this.storageGbPrice * this.formGroup.value.storageAdjustment; - } -} + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); + + this.dialogRef.close(this.ResultType.Submitted); + }; -/** - * Strongly typed helper to open an AdjustStorageDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustStorageDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustStorageDialogComponent, config); + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustStorageDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index b9c235943ad..9a69755b209 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -6,13 +6,10 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditDialogComponent } from "./add-credit-dialog.component"; -import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; -import { PaymentV2Component } from "./payment/payment-v2.component"; import { PaymentComponent } from "./payment/payment.component"; import { PaymentMethodComponent } from "./payment-method.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; @@ -26,40 +23,35 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac @NgModule({ imports: [ SharedModule, - PaymentComponent, TaxInfoComponent, HeaderModule, BannerModule, - PaymentV2Component, + PaymentComponent, VerifyBankAccountComponent, ], declarations: [ AddCreditDialogComponent, - AdjustPaymentDialogComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, PaymentMethodComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - AdjustPaymentDialogV2Component, - AdjustStorageDialogV2Component, + AdjustPaymentDialogComponent, + AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], exports: [ SharedModule, - PaymentComponent, TaxInfoComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, VerifyBankAccountComponent, - PaymentV2Component, + PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index 69a4b93bec8..54ab5bc0a2a 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -1,5 +1,4 @@ export * from "./billing-shared.module"; export * from "./payment-method.component"; -export * from "./payment/payment.component"; export * from "./sm-subscribe.component"; export * from "./tax-info.component"; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 149b4adf520..c5ec942f8b7 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -4,12 +4,16 @@ import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom, map } from "rxjs"; 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -25,8 +29,8 @@ import { TrialFlowService } from "../services/trial-flow.service"; import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { - AdjustPaymentDialogResult, - openAdjustPaymentDialog, + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ @@ -73,6 +77,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { private toastService: ToastService, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { const state = this.router.getCurrentNavigation()?.extras?.state; @@ -117,7 +122,14 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, @@ -158,14 +170,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { }; changePayment = async () => { - const dialogRef = openAdjustPaymentDialog(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { organizationId: this.organizationId, - currentType: this.paymentSource !== null ? this.paymentSource.type : null, + initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, }, }); + const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogResult.Adjusted) { + + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-label.component.html similarity index 100% rename from apps/web/src/app/billing/shared/payment/payment-label-v2.component.html rename to apps/web/src/app/billing/shared/payment/payment-label.component.html diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-label.component.ts similarity index 89% rename from apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts rename to apps/web/src/app/billing/shared/payment/payment-label.component.ts index f4d0f097766..179011e1144 100644 --- a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment-label.component.ts @@ -15,12 +15,12 @@ import { SharedModule } from "../../../shared"; * the `ExtensionRefresh` flag is set. */ @Component({ - selector: "app-payment-label-v2", - templateUrl: "./payment-label-v2.component.html", + selector: "app-payment-label", + templateUrl: "./payment-label.component.html", standalone: true, imports: [FormFieldModule, SharedModule], }) -export class PaymentLabelV2 implements OnInit { +export class PaymentLabelComponent implements OnInit { /** `id` of the associated input */ @Input({ required: true }) for: string; /** Displays required text on the label */ diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-v2.component.html deleted file mode 100644 index 9804e6bc86f..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.html +++ /dev/null @@ -1,152 +0,0 @@ -
-
- - - - - {{ "creditCard" | i18n }} - - - - - - {{ "bankAccount" | i18n }} - - - - - - {{ "payPal" | i18n }} - - - - - - {{ "accountCredit" | i18n }} - - - -
- - -
-
- - {{ "number" | i18n }} - -
-
-
- Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay -
-
- - {{ "expiration" | i18n }} - -
-
-
- - {{ "securityCodeSlashCVV" | i18n }} - - - - -
-
-
-
- - - - {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} - -
- - {{ "routingNumber" | i18n }} - - - - {{ "accountNumber" | i18n }} - - - - {{ "accountHolderName" | i18n }} - - - - {{ "bankAccountType" | i18n }} - - - - - - -
-
- - -
-
- {{ "paypalClickSubmit" | i18n }} -
-
- - - - {{ "makeSureEnoughCredit" | i18n }} - - - -
diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts deleted file mode 100644 index 10cf7ccb702..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ /dev/null @@ -1,193 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; - -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; - -import { SharedModule } from "../../../shared"; -import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; - -import { PaymentLabelV2 } from "./payment-label-v2.component"; - -/** - * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, - * optionally, submit it using the {@link onSubmit} function if it is provided. - * - * This component is meant to replace the existing {@link PaymentComponent} which is using the deprecated Stripe Sources API. - */ -@Component({ - selector: "app-payment-v2", - templateUrl: "./payment-v2.component.html", - standalone: true, - imports: [BillingServicesModule, SharedModule, PaymentLabelV2], -}) -export class PaymentV2Component implements OnInit, OnDestroy { - /** Show account credit as a payment option. */ - @Input() showAccountCredit: boolean = true; - /** Show bank account as a payment option. */ - @Input() showBankAccount: boolean = true; - /** Show PayPal as a payment option. */ - @Input() showPayPal: boolean = true; - - /** The payment method selected by default when the component renders. */ - @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; - /** If provided, will be invoked with the tokenized payment source during form submission. */ - @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - - @Output() submitted = new EventEmitter(); - - private destroy$ = new Subject(); - - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(null), - bankInformation: new FormGroup({ - routingNumber: new FormControl("", [Validators.required]), - accountNumber: new FormControl("", [Validators.required]), - accountHolderName: new FormControl("", [Validators.required]), - accountHolderType: new FormControl("", [Validators.required]), - }), - }); - - protected PaymentMethodType = PaymentMethodType; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private braintreeService: BraintreeService, - private stripeService: StripeService, - ) {} - - ngOnInit(): void { - this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); - - this.stripeService.loadStripe( - { - cardNumber: "#stripe-card-number", - cardExpiry: "#stripe-card-expiry", - cardCvc: "#stripe-card-cvc", - }, - this.initialPaymentMethod === PaymentMethodType.Card, - ); - - if (this.showPayPal) { - this.braintreeService.loadBraintree( - "#braintree-container", - this.initialPaymentMethod === PaymentMethodType.PayPal, - ); - } - - this.formGroup - .get("paymentMethod") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((type) => { - this.onPaymentMethodChange(type); - }); - } - - /** Programmatically select the provided payment method. */ - select = (paymentMethod: PaymentMethodType) => { - this.formGroup.get("paymentMethod").patchValue(paymentMethod); - }; - - protected submit = async () => { - const { type, token } = await this.tokenize(); - await this.onSubmit?.({ type, token }); - this.submitted.emit(type); - }; - - /** - * Tokenize the payment method information entered by the user against one of our payment providers. - * - * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} - * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} - * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} - * */ - async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { - const type = this.selected; - - if (this.usingStripe) { - const clientSecret = await this.billingApiService.createSetupIntent(type); - - if (this.usingBankAccount) { - const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { - accountHolderName: this.formGroup.value.bankInformation.accountHolderName, - routingNumber: this.formGroup.value.bankInformation.routingNumber, - accountNumber: this.formGroup.value.bankInformation.accountNumber, - accountHolderType: this.formGroup.value.bankInformation.accountHolderType, - }); - return { - type, - token, - }; - } - - if (this.usingCard) { - const token = await this.stripeService.setupCardPaymentMethod(clientSecret); - return { - type, - token, - }; - } - } - - if (this.usingPayPal) { - const token = await this.braintreeService.requestPaymentMethod(); - return { - type, - token, - }; - } - - return null; - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.stripeService.unloadStripe(); - if (this.showPayPal) { - this.braintreeService.unloadBraintree(); - } - } - - private onPaymentMethodChange(type: PaymentMethodType): void { - switch (type) { - case PaymentMethodType.Card: { - this.stripeService.mountElements(); - break; - } - case PaymentMethodType.PayPal: { - this.braintreeService.createDropin(); - break; - } - } - } - - get selected(): PaymentMethodType { - return this.formGroup.value.paymentMethod; - } - - protected get usingAccountCredit(): boolean { - return this.selected === PaymentMethodType.Credit; - } - - protected get usingBankAccount(): boolean { - return this.selected === PaymentMethodType.BankAccount; - } - - protected get usingCard(): boolean { - return this.selected === PaymentMethodType.Card; - } - - protected get usingPayPal(): boolean { - return this.selected === PaymentMethodType.PayPal; - } - - private get usingStripe(): boolean { - return this.usingBankAccount || this.usingCard; - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html index d4853713579..af261155171 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ b/apps/web/src/app/billing/shared/payment/payment.component.html @@ -1,96 +1,125 @@ -
-
- - +
+
+ + - {{ "creditCard" | i18n }} + {{ "creditCard" | i18n }} + - + - {{ "bankAccount" | i18n }} + {{ "bankAccount" | i18n }} + - - PayPal + + + + {{ "payPal" | i18n }} + - + - {{ "accountCredit" | i18n }} + {{ "accountCredit" | i18n }} +
- -
-
- {{ - "number" | i18n - }} -
+ + +
+
+ + {{ "number" | i18n }} + +
-
+
Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay
-
- {{ - "expiration" | i18n - }} -
+
+ + {{ "expiration" | i18n }} + +
-
- +
+ {{ "securityCodeSlashCVV" | i18n }} - -
+
+
- + + - {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} + {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} -
- +
+ {{ "routingNumber" | i18n }} - + - + {{ "accountNumber" | i18n }} - + - + {{ "accountHolderName" | i18n }} - - + {{ "bankAccountType" | i18n }} - +
- + +
-
+
{{ "paypalClickSubmit" | i18n }}
- - + + + {{ "makeSureEnoughCredit" | i18n }} - + -
+ + diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts index e067a5ee490..c11dfddb6cc 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment.component.ts @@ -1,330 +1,203 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; -import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; import { SharedModule } from "../../../shared"; +import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; -import { PaymentLabelV2 } from "./payment-label-v2.component"; +import { PaymentLabelComponent } from "./payment-label.component"; +/** + * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, + * optionally, submit it using the {@link onSubmit} function if it is provided. + */ @Component({ selector: "app-payment", - templateUrl: "payment.component.html", + templateUrl: "./payment.component.html", standalone: true, - imports: [SharedModule, PaymentLabelV2], + imports: [BillingServicesModule, SharedModule, PaymentLabelComponent], }) export class PaymentComponent implements OnInit, OnDestroy { - @Input() showMethods = true; - @Input() showOptions = true; - @Input() hideBank = false; - @Input() hidePaypal = false; - @Input() hideCredit = false; - @Input() trialFlow = false; + /** Show account credit as a payment option. */ + @Input() showAccountCredit: boolean = true; + /** Show bank account as a payment option. */ + @Input() showBankAccount: boolean = true; + /** Show PayPal as a payment option. */ + @Input() showPayPal: boolean = true; - @Input() - set method(value: PaymentMethodType) { - this._method = value; - this.paymentForm?.controls.method.setValue(value, { emitEvent: false }); - } + /** The payment method selected by default when the component renders. */ + @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; + /** If provided, will be invoked with the tokenized payment source during form submission. */ + @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - get method(): PaymentMethodType { - return this._method; - } - private _method: PaymentMethodType = PaymentMethodType.Card; + @Output() submitted = new EventEmitter(); private destroy$ = new Subject(); - protected paymentForm = new FormGroup({ - method: new FormControl(this.method), - bank: new FormGroup({ - routing_number: new FormControl(null, [Validators.required]), - account_number: new FormControl(null, [Validators.required]), - account_holder_name: new FormControl(null, [Validators.required]), - account_holder_type: new FormControl("", [Validators.required]), - currency: new FormControl("USD"), - country: new FormControl("US"), + + protected formGroup = new FormGroup({ + paymentMethod: new FormControl(null), + bankInformation: new FormGroup({ + routingNumber: new FormControl("", [Validators.required]), + accountNumber: new FormControl("", [Validators.required]), + accountHolderName: new FormControl("", [Validators.required]), + accountHolderType: new FormControl("", [Validators.required]), }), }); - paymentMethodType = PaymentMethodType; - private btScript: HTMLScriptElement; - private btInstance: any = null; - private stripeScript: HTMLScriptElement; - private stripe: any = null; - private stripeElements: any = null; - private stripeCardNumberElement: any = null; - private stripeCardExpiryElement: any = null; - private stripeCardCvcElement: any = null; - private StripeElementStyle: any; - private StripeElementClasses: any; + protected PaymentMethodType = PaymentMethodType; constructor( - private apiService: ApiService, - private logService: LogService, - private themingService: AbstractThemingService, - private configService: ConfigService, - ) { - this.stripeScript = window.document.createElement("script"); - this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false"; - this.stripeScript.async = true; - this.stripeScript.onload = async () => { - this.stripe = (window as any).Stripe(process.env.STRIPE_KEY); - this.stripeElements = this.stripe.elements(); - await this.setStripeElement(); - }; - this.btScript = window.document.createElement("script"); - this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`; - this.btScript.async = true; - this.StripeElementStyle = { - base: { - color: null, - fontFamily: - '"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' + - '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - fontSize: "16px", - fontSmoothing: "antialiased", - "::placeholder": { - color: null, - }, + private billingApiService: BillingApiServiceAbstraction, + private braintreeService: BraintreeService, + private stripeService: StripeService, + ) {} + + ngOnInit(): void { + this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); + + this.stripeService.loadStripe( + { + cardNumber: "#stripe-card-number", + cardExpiry: "#stripe-card-expiry", + cardCvc: "#stripe-card-cvc", }, - invalid: { - color: null, - }, - }; - this.StripeElementClasses = { - focus: "is-focused", - empty: "is-empty", - invalid: "is-invalid", - }; - } - async ngOnInit() { - if (!this.showOptions) { - this.hidePaypal = this.method !== PaymentMethodType.PayPal; - this.hideBank = this.method !== PaymentMethodType.BankAccount; - this.hideCredit = this.method !== PaymentMethodType.Credit; - } - this.subscribeToTheme(); - window.document.head.appendChild(this.stripeScript); - if (!this.hidePaypal) { - window.document.head.appendChild(this.btScript); + this.initialPaymentMethod === PaymentMethodType.Card, + ); + + if (this.showPayPal) { + this.braintreeService.loadBraintree( + "#braintree-container", + this.initialPaymentMethod === PaymentMethodType.PayPal, + ); } - this.paymentForm - .get("method") + + this.formGroup + .get("paymentMethod") .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((v) => { - this.method = v; - this.changeMethod(); + .subscribe((type) => { + this.onPaymentMethodChange(type); }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - window.document.head.removeChild(this.stripeScript); - window.setTimeout(() => { - Array.from(window.document.querySelectorAll("iframe")).forEach((el) => { - if (el.src != null && el.src.indexOf("stripe") > -1) { - try { - window.document.body.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - }, 500); - if (!this.hidePaypal) { - window.document.head.removeChild(this.btScript); - window.setTimeout(() => { - Array.from(window.document.head.querySelectorAll("script")).forEach((el) => { - if (el.src != null && el.src.indexOf("paypal") > -1) { - try { - window.document.head.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet"); - if (btStylesheet != null) { - try { - window.document.head.removeChild(btStylesheet); - } catch (e) { - this.logService.error(e); - } + /** Programmatically select the provided payment method. */ + select = (paymentMethod: PaymentMethodType) => { + this.formGroup.get("paymentMethod").patchValue(paymentMethod); + }; + + protected submit = async () => { + const { type, token } = await this.tokenize(); + await this.onSubmit?.({ type, token }); + this.submitted.emit(type); + }; + + /** + * Tokenize the payment method information entered by the user against one of our payment providers. + * + * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} + * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} + * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} + * */ + async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { + const type = this.selected; + + if (this.usingStripe) { + const clientSecret = await this.billingApiService.createSetupIntent(type); + + if (this.usingBankAccount) { + this.formGroup.markAllAsTouched(); + if (this.formGroup.valid) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } else { + throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; } - }, 500); + } + + if (this.usingCard) { + const token = await this.stripeService.setupCardPaymentMethod(clientSecret); + return { + type, + token, + }; + } } + + if (this.usingPayPal) { + const token = await this.braintreeService.requestPaymentMethod(); + return { + type, + token, + }; + } + + if (this.usingAccountCredit) { + return { + type: PaymentMethodType.Credit, + token: null, + }; + } + + return null; } - changeMethod() { - this.btInstance = null; - if (this.method === PaymentMethodType.PayPal) { - window.setTimeout(() => { - (window as any).braintree.dropin.create( - { - authorization: process.env.BRAINTREE_KEY, - container: "#bt-dropin-container", - paymentOptionPriority: ["paypal"], - paypal: { - flow: "vault", - buttonStyle: { - label: "pay", - size: "medium", - shape: "pill", - color: "blue", - tagline: "false", - }, - }, - }, - (createErr: any, instance: any) => { - if (createErr != null) { - // eslint-disable-next-line - console.error(createErr); - return; - } - this.btInstance = instance; - }, - ); - }, 250); - } else { - void this.setStripeElement(); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.stripeService.unloadStripe(); + if (this.showPayPal) { + this.braintreeService.unloadBraintree(); } } - createPaymentToken(): Promise<[string, PaymentMethodType]> { - return new Promise((resolve, reject) => { - if (this.method === PaymentMethodType.Credit) { - resolve([null, this.method]); - } else if (this.method === PaymentMethodType.PayPal) { - this.btInstance - .requestPaymentMethod() - .then((payload: any) => { - resolve([payload.nonce, this.method]); - }) - .catch((err: any) => { - reject(err.message); - }); - } else if ( - this.method === PaymentMethodType.Card || - this.method === PaymentMethodType.BankAccount - ) { - if (this.method === PaymentMethodType.Card) { - // 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.apiService - .postSetupPayment() - .then((clientSecret) => - this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement), - ) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.setupIntent && result.setupIntent.status === "succeeded") { - resolve([result.setupIntent.payment_method, this.method]); - } else { - reject(); - } - }); - } else { - this.stripe - .createToken("bank_account", this.paymentForm.get("bank").value) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.token && result.token.id != null) { - resolve([result.token.id, this.method]); - } else { - reject(); - } - }); - } + private onPaymentMethodChange(type: PaymentMethodType): void { + switch (type) { + case PaymentMethodType.Card: { + this.stripeService.mountElements(); + break; + } + case PaymentMethodType.PayPal: { + this.braintreeService.createDropin(); + break; } - }); + } } - handleStripeCardPayment(clientSecret: string, successCallback: () => Promise): Promise { - return new Promise((resolve, reject) => { - if (this.showMethods && this.stripeCardNumberElement == null) { - reject(); - return; - } - const handleCardPayment = () => - this.showMethods - ? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) - : this.stripe.handleCardSetup(clientSecret); - return handleCardPayment().then(async (result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.paymentIntent && result.paymentIntent.status === "succeeded") { - if (successCallback != null) { - await successCallback(); - } - resolve(); - } else { - reject(); - } - }); - }); + get selected(): PaymentMethodType { + return this.formGroup.value.paymentMethod; } - private async setStripeElement() { - const extensionRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); + protected get usingAccountCredit(): boolean { + return this.selected === PaymentMethodType.Credit; + } - // Apply unique styles for extension refresh - if (extensionRefreshFlag) { - this.StripeElementStyle.base.fontWeight = "500"; - this.StripeElementClasses.base = "v2"; - } + protected get usingBankAccount(): boolean { + return this.selected === PaymentMethodType.BankAccount; + } - window.setTimeout(() => { - if (this.showMethods && this.method === PaymentMethodType.Card) { - if (this.stripeCardNumberElement == null) { - this.stripeCardNumberElement = this.stripeElements.create("cardNumber", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - if (this.stripeCardExpiryElement == null) { - this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - }); - } - if (this.stripeCardCvcElement == null) { - this.stripeCardCvcElement = this.stripeElements.create("cardCvc", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - this.stripeCardNumberElement.mount("#stripe-card-number-element"); - this.stripeCardExpiryElement.mount("#stripe-card-expiry-element"); - this.stripeCardCvcElement.mount("#stripe-card-cvc-element"); - } - }, 50); + protected get usingCard(): boolean { + return this.selected === PaymentMethodType.Card; + } + + protected get usingPayPal(): boolean { + return this.selected === PaymentMethodType.PayPal; } - private subscribeToTheme() { - this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe(() => { - const style = getComputedStyle(document.documentElement); - this.StripeElementStyle.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.base["::placeholder"].color = `rgb(${style.getPropertyValue( - "--color-text-muted", - )})`; - this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue( - "--color-danger-600", - )})`; - }); + private get usingStripe(): boolean { + return this.usingBankAccount || this.usingCard; } } diff --git a/apps/web/src/app/common/base.accept.component.ts b/apps/web/src/app/common/base.accept.component.ts index 4e938fcd081..82a192ab0cf 100644 --- a/apps/web/src/app/common/base.accept.component.ts +++ b/apps/web/src/app/common/base.accept.component.ts @@ -5,7 +5,6 @@ import { ActivatedRoute, Params, Router } from "@angular/router"; import { Subject, firstValueFrom } from "rxjs"; import { first, switchMap, takeUntil } from "rxjs/operators"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -22,9 +21,6 @@ export abstract class BaseAcceptComponent implements OnInit { protected failedShortMessage = "inviteAcceptFailedShort"; protected failedMessage = "inviteAcceptFailed"; - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - private destroy$ = new Subject(); constructor( @@ -33,10 +29,10 @@ export abstract class BaseAcceptComponent implements OnInit { protected i18nService: I18nService, protected route: ActivatedRoute, protected authService: AuthService, - protected registerRouteService: RegisterRouteService, ) {} abstract authedHandler(qParams: Params): Promise; + abstract unauthedHandler(qParams: Params): Promise; async ngOnInit() { diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index a1fb1a8a0f3..91675bd4945 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -6,10 +6,9 @@ [attr.href]=" region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams " - class="pr-4" > diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8b..1581d4ad8cd 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -66,6 +66,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; @@ -73,7 +74,9 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; +import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -110,7 +113,7 @@ import { WebProcessReloadService } from "../key-management/services/web-process- import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; -import { WebSdkClientFactory } from "../platform/web-sdk-client-factory"; +import { WebSdkLoadService } from "../platform/web-sdk-load.service"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; import { EventService } from "./event.service"; @@ -288,9 +291,14 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCollectionAdminService, deps: [ApiService, KeyServiceAbstraction, EncryptService, CollectionService], }), + safeProvider({ + provide: SdkLoadService, + useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, + deps: [], + }), safeProvider({ provide: SdkClientFactory, - useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory, + useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, deps: [], }), safeProvider({ diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index b3e6d691f75..0d6063b4ac3 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -10,6 +10,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -35,11 +36,13 @@ export class InitService { private userAutoUnlockKeyService: UserAutoUnlockKeyService, private accountService: AccountService, private versionService: VersionService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); diff --git a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts index d407a709d4c..9358c4b200e 100644 --- a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts @@ -2,8 +2,14 @@ // @ts-strict-ignore import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request"; import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; diff --git a/apps/web/src/app/layouts/frontend-layout.component.html b/apps/web/src/app/layouts/frontend-layout.component.html index 72f0f1f1da3..d19af54f5df 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.html +++ b/apps/web/src/app/layouts/frontend-layout.component.html @@ -1,6 +1,8 @@ -
+ +
- © {{ year }} Bitwarden Inc.
- {{ "versionNumber" | i18n: version }} -
+ +
© {{ year }} Bitwarden Inc.
+
{{ version }}
+ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index f34d32f5983..ef702c7e593 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -10,7 +10,7 @@ @@ -27,7 +27,7 @@ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts index b09b32d060e..d64e1b817c1 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts @@ -3,11 +3,12 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { combineLatest, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import type { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { DialogService, NavigationModule } from "@bitwarden/components"; @@ -20,12 +21,17 @@ import { TrialFlowService } from "./../../billing/services/trial-flow.service"; imports: [CommonModule, JslibModule, NavigationModule], }) export class OrgSwitcherComponent { - protected organizations$: Observable = - this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), - ), - ); + protected organizations$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe( + map((orgs) => + orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), + ), + ), + ), + ); protected activeOrganization$: Observable = combineLatest([ this.route.paramMap, @@ -61,6 +67,7 @@ export class OrgSwitcherComponent { private organizationService: OrganizationService, private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, + private accountService: AccountService, ) {} protected toggle(event?: MouseEvent) { diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index 382ce8e026b..6e21c6c142a 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -7,6 +7,8 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { IconButtonModule, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index a7ff50b4264..181779c7c2e 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -1,16 +1,20 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherService } from "../shared/product-switcher.service"; @@ -22,11 +26,14 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +59,15 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + @Component({ selector: "story-layout", template: ``, @@ -86,6 +102,7 @@ export default { imports: [NavigationModule, RouterModule, LayoutComponent], providers: [ { provide: OrganizationService, useClass: MockOrganizationService }, + { provide: AccountService, useClass: MockAccountService }, { provide: ProviderService, useClass: MockProviderService }, { provide: SyncService, useClass: MockSyncService }, ProductSwitcherService, @@ -157,7 +174,6 @@ export const SMAvailable: Story = { canManageUsers: false, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -173,7 +189,6 @@ export const SMAndACAvailable: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -189,7 +204,6 @@ export const WithAllOptions: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [{ id: "provider-a" }] as Provider[], diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index 80155166394..834571e2cb4 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -1,5 +1,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; import { ProductSwitcherService } from "./shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index b53d0243f64..44467bb2b29 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -1,16 +1,20 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherContentComponent } from "./product-switcher-content.component"; @@ -22,11 +26,14 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +59,15 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + @Component({ selector: "story-layout", template: ``, @@ -78,6 +94,8 @@ export default { ], imports: [JslibModule, MenuModule, IconButtonModule, LinkModule, RouterModule], providers: [ + { provide: AccountService, useClass: MockAccountService }, + MockAccountService, { provide: OrganizationService, useClass: MockOrganizationService }, MockOrganizationService, { provide: ProviderService, useClass: MockProviderService }, @@ -134,7 +152,9 @@ export default { ], } as Meta; -type Story = StoryObj; +type Story = StoryObj< + ProductSwitcherComponent & MockProviderService & MockOrganizationService & MockAccountService +>; const Template: Story = { render: (args) => ({ @@ -176,7 +196,6 @@ export const WithSM: Story = { canManageUsers: false, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -192,7 +211,6 @@ export const WithSMAndAC: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -208,7 +226,6 @@ export const WithAllOptions: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [{ id: "provider-a" }] as Provider[], diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index a071d0f8852..4187900060b 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -10,7 +10,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ProductSwitcherService } from "./product-switcher.service"; @@ -19,8 +23,10 @@ describe("ProductSwitcherService", () => { let router: { url: string; events: Observable }; let organizationService: MockProxy; let providerService: MockProxy; + let accountService: FakeAccountService; let activeRouteParams = convertToParamMap({ organizationId: "1234" }); const getLastSync = jest.fn().mockResolvedValue(new Date("2024-05-14")); + const userId = Utils.newGuid() as UserId; // The service is dependent on the SyncService, which is behind a `setTimeout` // Most of the tests don't need to test this aspect so `advanceTimersByTime` @@ -36,10 +42,11 @@ describe("ProductSwitcherService", () => { router = mock(); organizationService = mock(); providerService = mock(); + accountService = mockAccountServiceWith(userId); router.url = "/"; router.events = of({}); - organizationService.organizations$ = of([{}] as Organization[]); + organizationService.organizations$.mockReturnValue(of([{}] as Organization[])); providerService.getAll.mockResolvedValue([] as Provider[]); TestBed.configureTestingModule({ @@ -47,6 +54,7 @@ describe("ProductSwitcherService", () => { { provide: Router, useValue: router }, { provide: OrganizationService, useValue: organizationService }, { provide: ProviderService, useValue: providerService }, + { provide: AccountService, useValue: accountService }, { provide: ActivatedRoute, useValue: { @@ -111,14 +119,15 @@ describe("ProductSwitcherService", () => { }); it("is included in bento when there is an organization with SM", async () => { - organizationService.organizations$ = of([ - { - id: "1234", - canAccessSecretsManager: true, - enabled: true, - canAccessExport: (_) => true, - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "1234", + canAccessSecretsManager: true, + enabled: true, + }, + ] as Organization[]), + ); initiateService(); @@ -139,7 +148,9 @@ describe("ProductSwitcherService", () => { }); it("includes Admin Console in bento when a user has access to it", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); initiateService(); @@ -195,7 +206,9 @@ describe("ProductSwitcherService", () => { }); it("marks Admin Console as active", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); activeRouteParams = convertToParamMap({ organizationId: "1" }); router.url = "/organizations/"; @@ -226,22 +239,22 @@ describe("ProductSwitcherService", () => { it("updates secrets manager path when the org id is found in the path", async () => { router.url = "/sm/4243"; - organizationService.organizations$ = of([ - { - id: "23443234", - canAccessSecretsManager: true, - enabled: true, - name: "Org 2", - canAccessExport: (_) => true, - }, - { - id: "4243", - canAccessSecretsManager: true, - enabled: true, - name: "Org 32", - canAccessExport: (_) => true, - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "23443234", + canAccessSecretsManager: true, + enabled: true, + name: "Org 2", + }, + { + id: "4243", + canAccessSecretsManager: true, + enabled: true, + name: "Org 32", + }, + ] as Organization[]), + ); initiateService(); @@ -256,10 +269,12 @@ describe("ProductSwitcherService", () => { it("updates admin console path when the org id is found in the path", async () => { router.url = "/organizations/111-22-33"; - organizationService.organizations$ = of([ - { id: "111-22-33", isOwner: true, name: "Test Org" }, - { id: "4243", isOwner: true, name: "My Org" }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { id: "111-22-33", isOwner: true, name: "Test Org" }, + { id: "4243", isOwner: true, name: "My Org" }, + ] as Organization[]), + ); initiateService(); diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index 2c16886a2d4..f962879d61a 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -2,7 +2,16 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { ActivatedRoute, NavigationEnd, NavigationStart, ParamMap, Router } from "@angular/router"; -import { combineLatest, concatMap, filter, map, Observable, ReplaySubject, startWith } from "rxjs"; +import { + combineLatest, + concatMap, + filter, + map, + Observable, + ReplaySubject, + startWith, + switchMap, +} from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { @@ -11,6 +20,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SyncService } from "@bitwarden/common/platform/sync"; export type ProductSwitcherItem = { @@ -90,18 +100,20 @@ export class ProductSwitcherService { private router: Router, private i18n: I18nPipe, private syncService: SyncService, + private accountService: AccountService, ) { this.pollUntilSynced(); } + organizations$ = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + switchMap((id) => this.organizationService.organizations$(id)), + ); + products$: Observable<{ bento: ProductSwitcherItem[]; other: ProductSwitcherItem[]; - }> = combineLatest([ - this.organizationService.organizations$, - this.route.paramMap, - this.triggerProductUpdate$, - ]).pipe( + }> = combineLatest([this.organizations$, this.route.paramMap, this.triggerProductUpdate$]).pipe( map(([orgs, ...rest]): [Organization[], ParamMap, void] => { return [ // Sort orgs by name to match the order within the sidebar diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index fadcc28f832..d03548faf9a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -9,9 +9,11 @@ import { redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, + activeAuthGuard, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; +import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -37,6 +39,8 @@ import { SsoComponent, VaultIcon, LoginDecryptionOptionsComponent, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; @@ -46,7 +50,6 @@ import { VaultIcons, } from "@bitwarden/vault"; -import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { flagEnabled, Flags } from "../utils/flags"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; @@ -71,7 +74,6 @@ import { SecurityRoutingModule } from "./auth/settings/security/security-routing import { SsoComponentV1 } from "./auth/sso-v1.component"; import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; -import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component"; import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component"; import { TwoFactorComponent } from "./auth/two-factor.component"; import { UpdatePasswordComponent } from "./auth/update-password.component"; @@ -96,6 +98,18 @@ import { SendComponent } from "./tools/send/send.component"; import { VaultModule } from "./vault/individual-vault/vault.module"; const routes: Routes = [ + // These need to be placed at the top of the list prior to the root + // so that the redirectGuard does not interrupt the navigation. + { + path: "register", + redirectTo: "signup", + pathMatch: "full", + }, + { + path: "trial", + redirectTo: "signup", + pathMatch: "full", + }, { path: "", component: FrontendLayoutComponent, @@ -112,20 +126,6 @@ const routes: Routes = [ component: LoginViaWebAuthnComponent, data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties, }, - { - path: "register", - component: TrialInitiationComponent, - canActivate: [ - canAccessFeature(FeatureFlag.EmailVerification, false, "/signup", false), - unauthGuardFn(), - ], - data: { titleId: "createAccount" } satisfies RouteDataProperties, - }, - { - path: "trial", - redirectTo: "register", - pathMatch: "full", - }, { path: "set-password", component: SetPasswordComponent, @@ -347,7 +347,7 @@ const routes: Routes = [ children: [ { path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationUserAddIcon, pageTitle: { @@ -372,7 +372,7 @@ const routes: Routes = [ }, { path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationLockAltIcon, titleId: "setAStrongPassword", @@ -406,7 +406,6 @@ const routes: Routes = [ }, { path: "set-password-jit", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { pageTitle: { @@ -419,7 +418,7 @@ const routes: Routes = [ }, { path: "signup-link-expired", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationExpiredLinkIcon, pageTitle: { @@ -544,12 +543,12 @@ const routes: Routes = [ } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn()], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, { path: "", @@ -586,6 +585,29 @@ const routes: Routes = [ titleId: "recoverAccountTwoStep", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, + { + path: "device-verification", + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [ + { + path: "", + component: NewDeviceVerificationComponent, + }, + ], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "accept-emergency", canActivate: [deepLinkGuard()], @@ -656,7 +678,7 @@ const routes: Routes = [ }, { path: "trial-initiation", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], component: CompleteTrialInitiationComponent, resolve: { pageTitle: freeTrialTextResolver, @@ -667,7 +689,7 @@ const routes: Routes = [ }, { path: "secrets-manager-trial-initiation", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], component: CompleteTrialInitiationComponent, resolve: { pageTitle: freeTrialTextResolver, diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts deleted file mode 100644 index 2ebb2bcc10f..00000000000 --- a/apps/web/src/app/platform/web-sdk-client-factory.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import * as sdk from "@bitwarden/sdk-internal"; - -/** - * SDK client factory with a js fallback for when WASM is not supported. - */ -export class WebSdkClientFactory implements SdkClientFactory { - async createSdkClient( - ...args: ConstructorParameters - ): Promise { - const module = await load(); - - (sdk as any).init(module); - - return Promise.resolve(new sdk.BitwardenClient(...args)); - } -} - -// https://stackoverflow.com/a/47880734 -const supported = (() => { - try { - if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { - const module = new WebAssembly.Module( - Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), - ); - if (module instanceof WebAssembly.Module) { - return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; - } - } - } catch (e) { - // ignore - } - return false; -})(); - -async function load() { - if (supported) { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); - } else { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); - } -} diff --git a/apps/web/src/app/platform/web-sdk-load.service.ts b/apps/web/src/app/platform/web-sdk-load.service.ts new file mode 100644 index 00000000000..cae3399b81e --- /dev/null +++ b/apps/web/src/app/platform/web-sdk-load.service.ts @@ -0,0 +1,31 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import * as sdk from "@bitwarden/sdk-internal"; + +// https://stackoverflow.com/a/47880734 +const supported = (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), + ); + if (module instanceof WebAssembly.Module) { + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } + } catch { + // ignore + } + return false; +})(); + +export class WebSdkLoadService implements SdkLoadService { + async load(): Promise { + let module: any; + if (supported) { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + } else { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); + } + (sdk as any).init(module); + } +} diff --git a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts index 805745c369d..5edd8bc046e 100644 --- a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts +++ b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Guid } from "@bitwarden/common/src/types/guid"; +import { Guid } from "@bitwarden/common/types/guid"; export class RequestSMAccessRequest { OrganizationId: Guid; diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts index 8909df6cf8a..d1ab7689cfe 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -3,9 +3,12 @@ import { Component, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Guid } from "@bitwarden/common/types/guid"; import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components"; @@ -39,10 +42,12 @@ export class RequestSMAccessComponent implements OnInit { private organizationService: OrganizationService, private smLandingApiService: SmLandingApiService, private toastService: ToastService, + private accountService: AccountService, ) {} async ngOnInit() { - this.organizations = (await this.organizationService.getAll()) + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = (await firstValueFrom(this.organizationService.organizations$(userId))) .filter((e) => e.enabled) .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts index 3698031a5b6..4d9dceab34a 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts @@ -1,9 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { NoItemsModule, SearchModule } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -22,10 +25,16 @@ export class SMLandingComponent implements OnInit { showSecretsManagerInformation: boolean = true; showGiveMembersAccessInstructions: boolean = false; - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + private accountService: AccountService, + ) {} async ngOnInit() { - const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const enabledOrganizations = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((e) => e.enabled); if (enabledOrganizations.length > 0) { this.handleEnabledOrganizations(enabledOrganizations); diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index a3bea63fb86..7e9e7d6ed64 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -43,7 +43,7 @@

{{ "customEqDomains" | i18n }}

-

{{ "globalEqDomains" | i18n }}

+

{{ "globalEqDomains" | i18n }}

this.organizationService.organizations$(account?.id)), + ); this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { this.organizations = orgs; }); diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 07dc218bd64..16541bdc109 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -7,7 +7,11 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("ExposedPasswordsReportComponent", () => { let auditService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); auditService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(userId); // 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 TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("ExposedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 13d2804c5e5..1a0d4043b74 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -25,6 +26,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected cipherService: CipherService, protected auditService: AuditService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -35,6 +37,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 80760eb5dec..385bda03f28 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -20,11 +24,14 @@ describe("InactiveTwoFactorReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(userId); // 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 TestBed.configureTestingModule({ @@ -38,6 +45,10 @@ describe("InactiveTwoFactorReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 792ad0616f2..52c52041c9d 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -27,6 +28,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, @@ -38,6 +40,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 9d16bbb1c62..6a26cd24fe5 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -6,7 +6,11 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -19,11 +23,15 @@ describe("ReusedPasswordsReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(userId); + // 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 TestBed.configureTestingModule({ @@ -37,6 +45,10 @@ describe("ReusedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index a8806acea13..a5c1c65560b 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -24,6 +25,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -34,6 +36,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5f66814fdf1..7cd159108b8 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -7,7 +7,11 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("UnsecuredWebsitesReportComponent", () => { let organizationService: MockProxy; let syncServiceMock: MockProxy; let collectionService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); collectionService = mock(); + accountService = mockAccountServiceWith(userId); // 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 TestBed.configureTestingModule({ @@ -40,6 +47,10 @@ describe("UnsecuredWebsitesReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 6a1ba1f6333..350e5c03980 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { CollectionService, Collection } from "@bitwarden/admin-console/common"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -22,6 +23,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -33,6 +35,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index bcace60ac0c..578c220f396 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("WeakPasswordsReportComponent", () => { let passwordStrengthService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); passwordStrengthService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(userId); // 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 TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("WeakPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index f3ad6840c8b..c374ecd0e4a 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -32,6 +33,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected organizationService: OrganizationService, + protected accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -42,6 +44,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index 80439acd510..ccfecdd197e 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -5,9 +5,7 @@ import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -58,9 +56,6 @@ export class AccessComponent implements OnInit { protected formGroup = this.formBuilder.group({}); - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - private id: string; private key: string; @@ -71,8 +66,6 @@ export class AccessComponent implements OnInit { private sendApiService: SendApiService, private toastService: ToastService, private i18nService: I18nService, - private configService: ConfigService, - private registerRouteService: RegisterRouteService, private layoutWrapperDataService: AnonLayoutWrapperDataService, protected formBuilder: FormBuilder, ) {} diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.html b/apps/web/src/app/tools/send/send-access-explainer.component.html index e8090cb850c..f56b68cc299 100644 --- a/apps/web/src/app/tools/send/send-access-explainer.component.html +++ b/apps/web/src/app/tools/send/send-access-explainer.component.html @@ -10,7 +10,7 @@ >Bitwarden Send {{ "sendAccessTaglineOr" | i18n }} - {{ + {{ "sendAccessTaglineSignUp" | i18n }} {{ "sendAccessTaglineTryToday" | i18n }} diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.ts b/apps/web/src/app/tools/send/send-access-explainer.component.ts index 756a1068985..e7106110a65 100644 --- a/apps/web/src/app/tools/send/send-access-explainer.component.ts +++ b/apps/web/src/app/tools/send/send-access-explainer.component.ts @@ -1,7 +1,5 @@ import { Component } from "@angular/core"; -import { RegisterRouteService } from "@bitwarden/auth/common"; - import { SharedModule } from "../../shared"; @Component({ @@ -11,7 +9,5 @@ import { SharedModule } from "../../shared"; imports: [SharedModule], }) export class SendAccessExplainerComponent { - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - constructor(private registerRouteService: RegisterRouteService) {} + constructor() {} } diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts index 0b2a971bbe8..b55e955f355 100644 --- a/apps/web/src/app/tools/send/send-access-file.component.ts +++ b/apps/web/src/app/tools/send/send-access-file.component.ts @@ -70,6 +70,8 @@ export class SendAccessFileComponent { blobData: decBuf, downloadMethod: "save", }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.toastService.showToast({ variant: "error", diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 42d033dc4c2..a035202516a 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -5,6 +5,7 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula import { AbstractControl, FormBuilder, Validators } from "@angular/forms"; import { combineLatest, + firstValueFrom, map, Observable, of, @@ -24,12 +25,17 @@ import { CollectionResponse, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { BitValidators, DialogService } from "@bitwarden/components"; +import { BitValidators, DialogService, ToastService } from "@bitwarden/components"; import { GroupApiService, GroupView } from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; @@ -110,6 +116,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, + private accountService: AccountService, + private toastService: ToastService, ) { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; } @@ -121,7 +129,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.selectedOrg.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((id) => this.loadOrg(id)); - this.organizations$ = this.organizationService.organizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organizations$ = this.organizationService.organizations$(userId).pipe( first(), map((orgs) => orgs @@ -139,8 +150,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } async loadOrg(orgId: string) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const organization$ = this.organizationService - .get$(orgId) + .organizations$(userId) + .pipe(getOrganizationById(orgId)) .pipe(shareReplay({ refCount: true, bufferSize: 1 })); const groups$ = organization$.pipe( switchMap((organization) => { @@ -274,17 +287,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired"); if (this.tabIndex === CollectionDialogTabType.Access && !accessTabError) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t( + "fieldOnTabRequiresAttention", + this.i18nService.t("collectionInfo"), + ), + }); } else if (this.tabIndex === CollectionDialogTabType.Info && accessTabError) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")), + }); } return; } @@ -309,14 +325,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const savedCollection = await this.collectionAdminService.save(collectionView); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t( + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( this.editMode ? "editedCollectionId" : "createdCollectionId", collectionView.name, ), - ); + }); this.close(CollectionDialogAction.Saved, savedCollection); }; @@ -339,11 +355,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { await this.collectionAdminService.delete(this.params.organizationId, this.params.collectionId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedCollectionId", this.collection?.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedCollectionId", this.collection?.name), + }); this.close(CollectionDialogAction.Deleted, this.collection); }; diff --git a/apps/web/src/app/vault/components/premium-badge.stories.ts b/apps/web/src/app/vault/components/premium-badge.stories.ts index 17622dbbd5f..331f72fd0ac 100644 --- a/apps/web/src/app/vault/components/premium-badge.stories.ts +++ b/apps/web/src/app/vault/components/premium-badge.stories.ts @@ -2,6 +2,7 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessageSender } from "@bitwarden/common/platform/messaging"; @@ -22,6 +23,14 @@ export default { moduleMetadata({ imports: [JslibModule, BadgeModule], providers: [ + { + provide: AccountService, + useValue: { + activeAccount$: of({ + id: "123", + }), + }, + }, { provide: I18nService, useFactory: () => { @@ -39,7 +48,7 @@ export default { { provide: BillingAccountProfileStateService, useValue: { - hasPremiumFromAnySource$: of(false), + hasPremiumFromAnySource$: () => of(false), }, }, ], diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index 56acc421de4..c19329f7121 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -36,7 +36,10 @@

- + + @@ -62,7 +65,7 @@ type="button" buttonType="secondary" (click)="cancel()" - *ngIf="!showCipherView" + *ngIf="!showCipherView && !showRestore" > {{ "cancel" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index c91314f68d7..a530fd0cc85 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -40,6 +40,7 @@ import { CipherFormGenerationService, CipherFormModule, CipherViewComponent, + DecryptionFailureDialogComponent, } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; @@ -48,6 +49,8 @@ import { AttachmentDialogResult, AttachmentsV2Component, } from "../../individual-vault/attachments-v2.component"; +import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; @@ -81,6 +84,11 @@ export interface VaultItemDialogParams { * If true, the dialog is being opened from the admin console. */ isAdminConsoleAction?: boolean; + + /** + * Function to restore a cipher from the trash. + */ + restore: (c: CipherView) => Promise; } export enum VaultItemDialogResult { @@ -98,6 +106,11 @@ export enum VaultItemDialogResult { * The dialog was closed to navigate the user the premium upgrade page. */ PremiumUpgrade = "premiumUpgrade", + + /** + * A cipher was restored + */ + Restored = "restored", } @Component({ @@ -114,11 +127,13 @@ export enum VaultItemDialogResult { CipherAttachmentsComponent, AsyncActionsModule, ItemModule, + DecryptionFailureDialogComponent, ], providers: [ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, + RoutedVaultFilterService, ], }) export class VaultItemDialogComponent implements OnInit, OnDestroy { @@ -189,6 +204,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ), ); + /** + * Determines if the user may restore the item. + * A user may restore items if they have delete permissions and the item is in the trash. + */ + protected async canUserRestore() { + return ( + this.filter?.type === "trash" && + this.cipher?.isDeleted && + (await firstValueFrom(this.canDeleteCipher$)) + ); + } + + protected showRestore: boolean; + protected get loadingForm() { return this.loadForm && !this.formReady; } @@ -228,6 +257,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected canDeleteCipher$: Observable; + protected filter: RoutedVaultFilterModel; + constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, private dialogRef: DialogRef, @@ -244,6 +275,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private cipherAuthorizationService: CipherAuthorizationService, private apiService: ApiService, private eventCollectionService: EventCollectionService, + private routedVaultFilterService: RoutedVaultFilterService, ) { this.updateTitle(); } @@ -252,6 +284,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.cipher = await this.getDecryptedCipherView(this.formConfig); if (this.cipher) { + if (this.cipher.decryptionFailure) { + this.dialogService.open(DecryptionFailureDialogComponent, { + data: { cipherIds: [this.cipher.id] }, + }); + this.dialogRef.close(); + return; + } + this.collections = this.formConfig.collections.filter((c) => this.cipher.collectionIds?.includes(c.id), ); @@ -273,6 +313,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ); } + this.filter = await firstValueFrom(this.routedVaultFilterService.filter$); + + this.showRestore = await this.canUserRestore(); this.performingInitialLoad = false; } @@ -326,6 +369,11 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this._formReadySubject.next(); } + restore = async () => { + await this.params.restore(this.cipher); + this.dialogRef.close(VaultItemDialogResult.Restored); + }; + delete = async () => { if (!this.cipher) { return; diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 5c4de576ead..7e59853851c 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -4,7 +4,7 @@ type="checkbox" bitCheckbox appStopProp - [disabled]="disabled" + [disabled]="disabled || cipher.decryptionFailure" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" [attr.aria-label]="'vaultItemSelect' | i18n" @@ -20,7 +20,7 @@ class="tw-overflow-hidden tw-text-ellipsis tw-text-start tw-leading-snug" [disabled]="disabled" [routerLink]="[]" - [queryParams]="{ itemId: cipher.id, action: extensionRefreshEnabled ? 'view' : null }" + [queryParams]="{ itemId: cipher.id, action: clickAction }" queryParamsHandling="merge" [replaceUrl]="extensionRefreshEnabled" title="{{ 'editItemWithName' | i18n: cipher.name }}" @@ -76,6 +76,25 @@ + + + +
+ + + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 80ed3025306..09195cd22ff 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,26 +1,37 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { firstValueFrom, from, map } from "rxjs"; -import { switchMap, takeUntil } from "rxjs/operators"; +import { debounceTime, first, switchMap } from "rxjs/operators"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.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"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; -import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -32,13 +43,26 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", + standalone: true, + imports: [ + SharedOrganizationModule, + HeaderModule, + CommonModule, + JslibModule, + AvatarModule, + RouterModule, + TableModule, + ], }) -export class ClientsComponent extends BaseClientsComponent implements OnInit, OnDestroy { - providerId: string; - addableOrganizations: Organization[]; +export class ClientsComponent { + providerId: string = ""; + addableOrganizations: Organization[] = []; loading = true; manageOrganizations = false; showAddExisting = false; + dataSource: TableDataSource = + new TableDataSource(); + protected searchControl = new FormControl("", { nonNullable: true }); constructor( private router: Router, @@ -46,28 +70,20 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On private apiService: ApiService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - activatedRoute: ActivatedRoute, - dialogService: DialogService, - i18nService: I18nService, - searchService: SearchService, - toastService: ToastService, - validationService: ValidationService, - webProviderService: WebProviderService, + private accountService: AccountService, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, ) { - super( - activatedRoute, - dialogService, - i18nService, - searchService, - toastService, - validationService, - webProviderService, - ); - } + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); - ngOnInit() { - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( @@ -85,23 +101,52 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On }), ); }), - takeUntil(this.destroy$), + takeUntilDestroyed(), ) .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); } - ngOnDestroy() { - super.ngOnDestroy(); + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } } async load() { const response = await this.apiService.getProviderClients(this.providerId); - this.clients = response.data != null && response.data.length > 0 ? response.data : []; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const clients = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); + const candidateOrgs = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((o) => o.isOwner && o.providerId == null); const allowedOrgsIds = await Promise.all( candidateOrgs.map((o) => this.organizationApiService.get(o.id)), ).then((orgs) => diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html deleted file mode 100644 index e6b5ae122f3..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - {{ "newClient" | i18n }} - - - - - - - {{ "loading" | i18n }} - - - -

{{ "noClientsInList" | i18n }}

- - - - {{ "name" | i18n }} - {{ "numberOfUsers" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - - {{ row.organizationName }} - - - {{ row.userCount }} - / {{ row.seats }} - - - {{ row.plan }} - - - - - - - -
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts deleted file mode 100644 index 2be38477d4c..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router, RouterModule } from "@angular/router"; -import { firstValueFrom, from, map } from "rxjs"; -import { debounceTime, first, switchMap } from "rxjs/operators"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -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"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../services/web-provider.service"; - -import { AddOrganizationComponent } from "./add-organization.component"; - -const DisallowedPlanTypes = [ - PlanType.Free, - PlanType.FamiliesAnnually2019, - PlanType.FamiliesAnnually, - PlanType.TeamsStarter2023, - PlanType.TeamsStarter, -]; - -@Component({ - templateUrl: "vnext-clients.component.html", - standalone: true, - imports: [ - SharedOrganizationModule, - HeaderModule, - CommonModule, - JslibModule, - AvatarModule, - RouterModule, - TableModule, - ], -}) -export class vNextClientsComponent { - providerId: string = ""; - addableOrganizations: Organization[] = []; - loading = true; - manageOrganizations = false; - showAddExisting = false; - dataSource: TableDataSource = - new TableDataSource(); - protected searchControl = new FormControl("", { nonNullable: true }); - - constructor( - private router: Router, - private providerService: ProviderService, - private apiService: ApiService, - private organizationService: OrganizationService, - private organizationApiService: OrganizationApiServiceAbstraction, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.activatedRoute.parent?.params - ?.pipe( - switchMap((params) => { - this.providerId = params.providerId; - return this.providerService.get$(this.providerId).pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), - map((isBillable) => { - if (isBillable) { - return from( - this.router.navigate(["../manage-client-organizations"], { - relativeTo: this.activatedRoute, - }), - ); - } else { - return from(this.load()); - } - }), - ); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.webProviderService.detachOrganization(this.providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } - - async load() { - const response = await this.apiService.getProviderClients(this.providerId); - const clients = response.data != null && response.data.length > 0 ? response.data : []; - this.dataSource.data = clients; - this.manageOrganizations = - (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); - const allowedOrgsIds = await Promise.all( - candidateOrgs.map((o) => this.organizationApiService.get(o.id)), - ).then((orgs) => - orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id), - ); - this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id)); - - this.showAddExisting = this.addableOrganizations.length !== 0; - this.loading = false; - } - - async addExistingOrganization() { - const dialogRef = AddOrganizationComponent.open(this.dialogService, { - providerId: this.providerId, - organizations: this.addableOrganizations, - }); - - if (await firstValueFrom(dialogRef.closed)) { - await this.load(); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index bd8c54c39d7..56584b67f02 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -2,9 +2,7 @@ // @ts-strict-ignore import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -33,9 +31,8 @@ export class AcceptProviderComponent extends BaseAcceptComponent { authService: AuthService, private apiService: ApiService, platformUtilService: PlatformUtilsService, - registerRouteService: RegisterRouteService, ) { - super(router, platformUtilService, i18nService, route, authService, registerRouteService); + super(router, platformUtilService, i18nService, route, authService); } async authedHandler(qParams: Params) { @@ -47,6 +44,7 @@ export class AcceptProviderComponent extends BaseAcceptComponent { qParams.providerUserId, request, ); + this.platformUtilService.showToast( "success", this.i18nService.t("inviteAccepted"), @@ -64,25 +62,14 @@ export class AcceptProviderComponent extends BaseAcceptComponent { } async register() { - let queryParams: Params; - let registerRoute = await firstValueFrom(this.registerRoute$); - if (registerRoute === "/register") { - queryParams = { - email: this.email, - }; - } else if (registerRoute === "/signup") { - // We have to override the base component route as we don't need users to - // complete email verification if they are coming directly an emailed invite. - registerRoute = "/finish-signup"; - queryParams = { + // We don't need users to complete email verification if they are coming directly from an emailed invite. + // Therefore, we skip /signup and navigate directly to /finish-signup. + await this.router.navigate(["/finish-signup"], { + queryParams: { email: this.email, providerUserId: this.providerUserId, providerInviteToken: this.providerInviteToken, - }; - } - - await this.router.navigate([registerRoute], { - queryParams: queryParams, + }, }); } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 09276263332..00c944e69bb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,10 +2,8 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -14,12 +12,10 @@ import { ProviderSubscriptionComponent, hasConsolidatedBilling, ProviderBillingHistoryComponent, - vNextManageClientsComponent, } from "../../billing/providers"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; -import { vNextClientsComponent } from "./clients/vnext-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; @@ -86,25 +82,13 @@ const routes: Routes = [ children: [ { path: "", pathMatch: "full", redirectTo: "clients" }, { path: "clients/create", component: CreateOrganizationComponent }, - ...featureFlaggedRoute({ - defaultComponent: ClientsComponent, - flaggedComponent: vNextClientsComponent, - featureFlag: FeatureFlag.PM12443RemovePagingLogic, - routeOptions: { - path: "clients", - data: { titleId: "clients" }, - }, - }), - ...featureFlaggedRoute({ - defaultComponent: ManageClientsComponent, - flaggedComponent: vNextManageClientsComponent, - featureFlag: FeatureFlag.PM12443RemovePagingLogic, - routeOptions: { - path: "manage-client-organizations", - data: { titleId: "clients" }, - canActivate: [hasConsolidatedBilling], - }, - }), + { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, + { + path: "manage-client-organizations", + canActivate: [hasConsolidatedBilling], + component: ManageClientsComponent, + data: { titleId: "clients" }, + }, { path: "manage", children: [ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 80108e66eda..37cb9618b60 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -11,9 +11,7 @@ import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { CreateClientDialogComponent, - NoClientsComponent, ManageClientNameDialogComponent, - ManageClientsComponent, ManageClientSubscriptionDialogComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, @@ -21,7 +19,6 @@ import { } from "../../billing/providers"; import { AddOrganizationComponent } from "./clients/add-organization.component"; -import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; @@ -59,7 +56,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr AddOrganizationComponent, BulkConfirmDialogComponent, BulkRemoveDialogComponent, - ClientsComponent, CreateOrganizationComponent, EventsComponent, MembersComponent, @@ -68,8 +64,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr UserAddEditComponent, AddEditMemberDialogComponent, CreateClientDialogComponent, - NoClientsComponent, - ManageClientsComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, ProviderBillingHistoryComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index f773db6c11c..ecf649b8f31 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -23,8 +23,7 @@ import { KeyService } from "@bitwarden/key-management"; templateUrl: "setup.component.html", }) export class SetupComponent implements OnInit, OnDestroy { - @ViewChild(ManageTaxInformationComponent) - manageTaxInformationComponent: ManageTaxInformationComponent; + @ViewChild(ManageTaxInformationComponent) taxInformationComponent: ManageTaxInformationComponent; loading = true; providerId: string; @@ -111,7 +110,7 @@ export class SetupComponent implements OnInit, OnDestroy { try { this.formGroup.markAllAsTouched(); - if (!this.manageTaxInformationComponent.validate() || !this.formGroup.valid) { + if (!this.taxInformationComponent.validate() || !this.formGroup.valid) { return; } @@ -125,7 +124,7 @@ export class SetupComponent implements OnInit, OnDestroy { request.key = key; request.taxInfo = new ExpandedTaxInfoUpdateRequest(); - const taxInformation = this.manageTaxInformationComponent.getTaxInformation(); + const taxInformation = this.taxInformationComponent.getTaxInformation(); request.taxInfo.country = taxInformation.country; request.taxInfo.postalCode = taxInformation.postalCode; @@ -147,6 +146,7 @@ export class SetupComponent implements OnInit, OnDestroy { await this.router.navigate(["/providers", provider.id]); } catch (e) { + e.message = this.i18nService.translate(e.message) || e.message; this.validationService.showError(e); } }; diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 0731820e413..036163af3d9 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -89,19 +89,19 @@ {{ "trustedDevices" | i18n }} - {{ "memberDecryptionOptionTdeDescriptionPartOne" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkOne" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartTwo" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkTwo" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartThree" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkThree" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartFour" | i18n }} + {{ "memberDecryptionOptionTdeDescPart1" | i18n }} + + {{ "memberDecryptionOptionTdeDescLink1" | i18n }} + + {{ "memberDecryptionOptionTdeDescPart2" | i18n }} + + {{ "memberDecryptionOptionTdeDescLink2" | i18n }} + + {{ "memberDecryptionOptionTdeDescPart3" | i18n }} + + {{ "memberDecryptionOptionTdeDescLink3" | i18n }} + + {{ "memberDecryptionOptionTdeDescPart4" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 6449ef7a701..779cb96146c 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,13 +9,17 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MemberDecryptionType, OpenIdConnectRedirectBehavior, @@ -28,6 +32,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -195,6 +200,7 @@ export class SsoComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private toastService: ToastService, @@ -260,7 +266,12 @@ export class SsoComponent implements OnInit, OnDestroy { } async load() { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const ssoSettings = await this.organizationApiService.getSso(this.organizationId); this.populateForm(ssoSettings); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index 78f2cb41bef..c11b23db9fb 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -15,7 +15,7 @@
@@ -29,9 +29,11 @@

{{ planCard.name }}

{{ - planCard.cost | currency: "$" + planCard.getMonthlyCost() | currency: "$" }} - / {{ "monthPerMember" | i18n }} + / {{ planCard.getTimePerMemberLabel() | i18n }}
@@ -45,8 +47,8 @@

{{ planCard.name }}

{{ "organizationNameMaxLength" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index 2a27b1b32f3..38eaed83937 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -1,6 +1,5 @@ -// 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 { BasePortalOutlet } from "@angular/cdk/portal"; import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; @@ -25,48 +24,42 @@ export enum CreateClientDialogResultType { export const openCreateClientDialog = ( dialogService: DialogService, - dialogConfig: DialogConfig, + dialogConfig: DialogConfig< + CreateClientDialogParams, + DialogRef, + BasePortalOutlet + >, ) => dialogService.open( CreateClientDialogComponent, dialogConfig, ); -type PlanCard = { - name: string; - cost: number; - type: PlanType; - plan: PlanResponse; +export class PlanCard { + readonly name: string; + private readonly cost: number; + readonly type: PlanType; + readonly plan: PlanResponse; selected: boolean; -}; -@Component({ - templateUrl: "./create-client-dialog.component.html", -}) -export class CreateClientDialogComponent implements OnInit { - protected discountPercentage: number; - protected formGroup = new FormGroup({ - clientOwnerEmail: new FormControl("", [Validators.required, Validators.email]), - organizationName: new FormControl("", [Validators.required, Validators.maxLength(50)]), - seats: new FormControl(null, [Validators.required, Validators.min(1)]), - }); - protected loading = true; - protected planCards: PlanCard[]; - protected ResultType = CreateClientDialogResultType; + constructor(name: string, cost: number, type: PlanType, plan: PlanResponse, selected: boolean) { + this.name = name; + this.cost = cost; + this.type = type; + this.plan = plan; + this.selected = selected; + } - private providerPlans: ProviderPlanResponse[]; + getMonthlyCost(): number { + return this.plan.isAnnual ? this.cost / 12 : this.cost; + } - constructor( - private billingApiService: BillingApiServiceAbstraction, - @Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - private webProviderService: WebProviderService, - ) {} + getTimePerMemberLabel(): string { + return this.plan.isAnnual ? "monthPerMemberBilledAnnually" : "monthPerMember"; + } - protected getPlanCardContainerClasses(selected: boolean) { - switch (selected) { + getContainerClasses() { + switch (this.selected) { case true: { return [ "tw-group/plan-card-container", @@ -97,6 +90,41 @@ export class CreateClientDialogComponent implements OnInit { } } } +} + +@Component({ + templateUrl: "./create-client-dialog.component.html", +}) +export class CreateClientDialogComponent implements OnInit { + protected discountPercentage: number | null | undefined; + protected formGroup = new FormGroup({ + clientOwnerEmail: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.email], + }), + organizationName: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(50)], + }), + seats: new FormControl(1, { + nonNullable: true, + validators: [Validators.required, Validators.min(1)], + }), + }); + protected loading = true; + protected planCards: PlanCard[] = []; + protected ResultType = CreateClientDialogResultType; + + private providerPlans: ProviderPlanResponse[] = []; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + @Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private toastService: ToastService, + private webProviderService: WebProviderService, + ) {} async ngOnInit(): Promise { const response = await this.billingApiService.getProviderSubscription( @@ -114,6 +142,10 @@ export class CreateClientDialogComponent implements OnInit { const providerPlan = this.providerPlans[i]; const plan = this.dialogParams.plans.find((plan) => plan.type === providerPlan.type); + if (!plan) { + continue; + } + let planName: string; switch (plan.productTier) { case ProductTierType.Teams: { @@ -124,23 +156,28 @@ export class CreateClientDialogComponent implements OnInit { planName = this.i18nService.t("planNameEnterprise"); break; } + default: + continue; } - this.planCards.push({ - name: planName, - cost: plan.PasswordManager.providerPortalSeatPrice * discountFactor, - type: plan.type, - plan: plan, - selected: i === 0, - }); + this.planCards.push( + new PlanCard( + planName, + plan.PasswordManager.providerPortalSeatPrice * discountFactor, + plan.type, + plan, + i === 0, + ), + ); } this.loading = false; } protected selectPlan(name: string) { - this.planCards.find((planCard) => planCard.name === name).selected = true; - this.planCards.find((planCard) => planCard.name !== name).selected = false; + this.planCards.forEach((planCard) => { + planCard.selected = planCard.name === name; + }); } submit = async () => { @@ -152,17 +189,21 @@ export class CreateClientDialogComponent implements OnInit { const selectedPlanCard = this.planCards.find((planCard) => planCard.selected); + if (!selectedPlanCard) { + return; + } + await this.webProviderService.createClientOrganization( this.dialogParams.providerId, - this.formGroup.value.organizationName, - this.formGroup.value.clientOwnerEmail, + this.formGroup.controls.organizationName.value, + this.formGroup.controls.clientOwnerEmail.value, selectedPlanCard.type, - this.formGroup.value.seats, + this.formGroup.controls.seats.value, ); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("createdNewClient"), }); @@ -178,7 +219,7 @@ export class CreateClientDialogComponent implements OnInit { const openSeats = selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats; - const unassignedSeats = openSeats - this.formGroup.value.seats; + const unassignedSeats = openSeats - this.formGroup.controls.seats.value; return unassignedSeats > 0 ? unassignedSeats : 0; } @@ -191,22 +232,22 @@ export class CreateClientDialogComponent implements OnInit { } if (selectedProviderPlan.purchasedSeats > 0) { - return this.formGroup.value.seats; + return this.formGroup.controls.seats.value; } const additionalSeatsPurchased = - this.formGroup.value.seats + + this.formGroup.controls.seats.value + selectedProviderPlan.assignedSeats - selectedProviderPlan.seatMinimum; return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0; } - private getSelectedProviderPlan(): ProviderPlanResponse { + private getSelectedProviderPlan(): ProviderPlanResponse | null { if (this.loading || !this.planCards) { return null; } - const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan; - return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name); + const selectedPlan = this.planCards.find((planCard) => planCard.selected)!.plan; + return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name)!; } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts index 05887fc198e..898d51e0baf 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -3,5 +3,4 @@ export * from "./manage-clients.component"; export * from "./manage-client-name-dialog.component"; export * from "./manage-client-subscription-dialog.component"; export * from "./no-clients.component"; -export * from "./vnext-manage-clients.component"; export * from "./replace.pipe"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html index 49a9208107f..7c560e49579 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html @@ -1,5 +1,5 @@ - + {{ "addNewOrganization" | i18n }} @@ -16,78 +16,67 @@ - + - - {{ "client" | i18n }} - {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} - {{ "billingPlan" | i18n }} - - + {{ "client" | i18n }} + {{ "assigned" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} + {{ "billingPlan" | i18n }} + - - - - - - - - - - {{ client.seats }} - - - {{ client.occupiedSeats }} - - - {{ client.remainingSeats }} - - - {{ removeMonthly(client.plan) }} - - - - - - - - - - + + + + + + + + + {{ row.seats }} + + + {{ row.occupiedSeats }} + + + {{ row.remainingSeats }} + + + {{ row.plan | replace: " (Monthly)" : "" }} + + + + + + + + + - -
+ +
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index d413551b743..ee2c541e72f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -1,23 +1,28 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { debounceTime, first, switchMap } from "rxjs/operators"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; import { @@ -32,47 +37,53 @@ import { ManageClientSubscriptionDialogResultType, openManageClientSubscriptionDialog, } from "./manage-client-subscription-dialog.component"; +import { NoClientsComponent } from "./no-clients.component"; +import { ReplacePipe } from "./replace.pipe"; @Component({ templateUrl: "manage-clients.component.html", + standalone: true, + imports: [ + AvatarModule, + TableModule, + HeaderModule, + SharedOrganizationModule, + NoClientsComponent, + ReplacePipe, + ], }) -export class ManageClientsComponent extends BaseClientsComponent { - providerId: string; - provider: Provider; - +export class ManageClientsComponent { + providerId: string = ""; + provider: Provider | undefined; loading = true; isProviderAdmin = false; + dataSource: TableDataSource = + new TableDataSource(); - protected plans: PlanResponse[]; + protected searchControl = new FormControl("", { nonNullable: true }); + protected plans: PlanResponse[] = []; constructor( - private billingApiService: BillingApiService, + private billingApiService: BillingApiServiceAbstraction, private providerService: ProviderService, private router: Router, - activatedRoute: ActivatedRoute, - dialogService: DialogService, - i18nService: I18nService, - searchService: SearchService, - toastService: ToastService, - validationService: ValidationService, - webProviderService: WebProviderService, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, ) { - super( - activatedRoute, - dialogService, - i18nService, - searchService, - toastService, - validationService, - webProviderService, - ); - - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), map((isBillable) => { if (!isBillable) { return from( @@ -89,20 +100,24 @@ export class ManageClientsComponent extends BaseClientsComponent { takeUntilDestroyed(), ) .subscribe(); - } - removeMonthly = (plan: string) => plan.replace(" (Monthly)", ""); + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } async load() { this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - this.isProviderAdmin = this.provider.type === ProviderUserType.ProviderAdmin; + this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; - this.clients = ( - await this.billingApiService.getProviderClientOrganizations(this.providerId) - ).data; + const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) + .data; - this.dataSource.data = this.clients; + this.dataSource.data = clients; this.plans = (await this.billingApiService.getPlans()).data; @@ -131,7 +146,7 @@ export class ManageClientsComponent extends BaseClientsComponent { organization: { id: organization.id, name: organization.organizationName, - seats: organization.seats, + seats: organization.seats ? organization.seats : 0, }, }, }); @@ -149,7 +164,7 @@ export class ManageClientsComponent extends BaseClientsComponent { const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { data: { organization, - provider: this.provider, + provider: this.provider!, }, }); @@ -159,4 +174,28 @@ export class ManageClientsComponent extends BaseClientsComponent { await this.load(); } }; + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts index 30ab444fe8b..7e4323d7603 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { svgIcon } from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; const gearIcon = svgIcon` @@ -23,6 +24,8 @@ const gearIcon = svgIcon` @Component({ selector: "app-no-clients", + standalone: true, + imports: [SharedOrganizationModule], template: `

{{ "noClients" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html deleted file mode 100644 index 7c560e49579..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - {{ "addNewOrganization" | i18n }} - - - - - - {{ "loading" | i18n }} - - - - - - {{ "client" | i18n }} - {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - - - - - {{ row.seats }} - - - {{ row.occupiedSeats }} - - - {{ row.remainingSeats }} - - - {{ row.plan | replace: " (Monthly)" : "" }} - - - - - - - - - - - -
- -
-
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts deleted file mode 100644 index 94f615f0cee..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; -import { debounceTime, first, switchMap } from "rxjs/operators"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; - -import { - CreateClientDialogResultType, - openCreateClientDialog, -} from "./create-client-dialog.component"; -import { - ManageClientNameDialogResultType, - openManageClientNameDialog, -} from "./manage-client-name-dialog.component"; -import { - ManageClientSubscriptionDialogResultType, - openManageClientSubscriptionDialog, -} from "./manage-client-subscription-dialog.component"; -import { ReplacePipe } from "./replace.pipe"; -import { vNextNoClientsComponent } from "./vnext-no-clients.component"; - -@Component({ - templateUrl: "vnext-manage-clients.component.html", - standalone: true, - imports: [ - AvatarModule, - TableModule, - HeaderModule, - SharedOrganizationModule, - vNextNoClientsComponent, - ReplacePipe, - ], -}) -export class vNextManageClientsComponent { - providerId: string = ""; - provider: Provider | undefined; - loading = true; - isProviderAdmin = false; - dataSource: TableDataSource = - new TableDataSource(); - - protected searchControl = new FormControl("", { nonNullable: true }); - protected plans: PlanResponse[] = []; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private providerService: ProviderService, - private router: Router, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.activatedRoute.parent?.params - ?.pipe( - switchMap((params) => { - this.providerId = params.providerId; - return this.providerService.get$(this.providerId).pipe( - map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), - map((isBillable) => { - if (!isBillable) { - return from( - this.router.navigate(["../clients"], { - relativeTo: this.activatedRoute, - }), - ); - } else { - return from(this.load()); - } - }), - ); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async load() { - this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - - this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; - - const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) - .data; - - this.dataSource.data = clients; - - this.plans = (await this.billingApiService.getPlans()).data; - - this.loading = false; - } - - createClient = async () => { - const reference = openCreateClientDialog(this.dialogService, { - data: { - providerId: this.providerId, - plans: this.plans, - }, - }); - - const result = await lastValueFrom(reference.closed); - - if (result === CreateClientDialogResultType.Submitted) { - await this.load(); - } - }; - - manageClientName = async (organization: ProviderOrganizationOrganizationDetailsResponse) => { - const dialogRef = openManageClientNameDialog(this.dialogService, { - data: { - providerId: this.providerId, - organization: { - id: organization.id, - name: organization.organizationName, - seats: organization.seats ? organization.seats : 0, - }, - }, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === ManageClientNameDialogResultType.Submitted) { - await this.load(); - } - }; - - manageClientSubscription = async ( - organization: ProviderOrganizationOrganizationDetailsResponse, - ) => { - const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { - data: { - organization, - provider: this.provider!, - }, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === ManageClientSubscriptionDialogResultType.Submitted) { - await this.load(); - } - }; - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.webProviderService.detachOrganization(this.providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts deleted file mode 100644 index 5ad19945c51..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; - -import { svgIcon } from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; - -const gearIcon = svgIcon` - - - - - - - - - - - - - - - - -`; - -@Component({ - selector: "app-no-clients", - standalone: true, - imports: [SharedOrganizationModule], - template: `
- -

{{ "noClients" | i18n }}

- - - {{ "addNewOrganization" | i18n }} - -
`, -}) -export class vNextNoClientsComponent { - icon = gearIcon; - @Input() showAddOrganizationButton = true; - @Output() addNewOrganizationClicked = new EventEmitter(); - - addNewOrganization = () => this.addNewOrganizationClicked.emit(); -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 50db2cb6a36..72f743d2cc7 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -25,7 +25,7 @@ {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ - i.cadence.toLowerCase() + getFormattedPlanNameCadence(i.cadence) | i18n }}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }} @ {{ @@ -38,12 +38,11 @@ }} - {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{ - "month" | i18n - }} + {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} / + {{ getBillingCadenceLabel(i) | i18n }}
- {{ i.cost | currency: "$" }} /{{ "month" | i18n }} + {{ i.cost | currency: "$" }} / {{ getBillingCadenceLabel(i) | i18n }}
@@ -52,8 +51,9 @@ - Total: {{ totalCost | currency: "$" }} /{{ - "month" | i18n + Total: {{ totalCost | currency: "$" }} / + {{ + getBillingCadenceLabel(activePlans.length > 0 ? activePlans[0] : null) | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index a6b27b9f3dd..02fa29578d9 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -19,14 +19,13 @@ import { ToastService } from "@bitwarden/components"; templateUrl: "./provider-subscription.component.html", }) export class ProviderSubscriptionComponent implements OnInit, OnDestroy { - providerId: string; - subscription: ProviderSubscriptionResponse; + private providerId: string; + protected subscription: ProviderSubscriptionResponse; - firstLoaded = false; - loading: boolean; + protected firstLoaded = false; + protected loading: boolean; private destroy$ = new Subject(); - totalCost: number; - currentDate = new Date(); + protected totalCost: number; protected readonly TaxInformation = TaxInformation; @@ -50,11 +49,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { .subscribe(); } - get isExpired() { - return this.subscription.status !== "active"; - } - - async load() { + protected async load() { if (this.loading) { return; } @@ -65,7 +60,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { this.loading = false; } - updateTaxInformation = async (taxInformation: TaxInformation) => { + protected updateTaxInformation = async (taxInformation: TaxInformation) => { const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); await this.billingApiService.updateProviderTaxInformation(this.providerId, request); this.toastService.showToast({ @@ -75,7 +70,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { }); }; - getFormattedCost( + protected getFormattedCost( cost: number, seatMinimum: number, purchasedSeats: number, @@ -85,17 +80,21 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { return costPerSeat - (costPerSeat * discountPercentage) / 100; } - getFormattedPlanName(planName: string): string { + protected getFormattedPlanName(planName: string): string { const spaceIndex = planName.indexOf(" "); return planName.substring(0, spaceIndex); } - getFormattedSeatCount(seatMinimum: number, purchasedSeats: number): string { + protected getFormattedSeatCount(seatMinimum: number, purchasedSeats: number): string { const totalSeats = seatMinimum + purchasedSeats; return totalSeats > 1 ? totalSeats.toString() : ""; } - sumCost(plans: ProviderPlanResponse[]): number { + protected getFormattedPlanNameCadence(cadence: string) { + return cadence === "Annual" ? "annually" : "monthly"; + } + + private sumCost(plans: ProviderPlanResponse[]): number { return plans.reduce((acc, plan) => acc + plan.cost, 0); } @@ -104,7 +103,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - get activePlans(): ProviderPlanResponse[] { + protected get activePlans(): ProviderPlanResponse[] { return this.subscription.plans.filter((plan) => { if (plan.purchasedSeats === 0) { return plan.seatMinimum > 0; @@ -113,4 +112,19 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } }); } + + protected getBillingCadenceLabel(providerPlanResponse: ProviderPlanResponse): string { + if (providerPlanResponse == null || providerPlanResponse == undefined) { + return "month"; + } + + switch (providerPlanResponse.cadence) { + case "Monthly": + return "month"; + case "Annual": + return "year"; + default: + return "month"; + } + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts index a1f7564156c..d042c4d904e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts @@ -1,7 +1,13 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -10,13 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv export const organizationEnabledGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const org = await orgService.get(route.params.organizationId); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const org = await firstValueFrom( + orgService.organizations$(userId).pipe(getOrganizationById(route.params.organizationId)), + ); if (org == null || !org.canAccessSecretsManager) { return createUrlTreeFromSnapshot(route, ["/"]); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts index 2a36bf1cbbc..39576bc8dc6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts @@ -5,8 +5,11 @@ import { createUrlTreeFromSnapshot, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -18,13 +21,15 @@ export const canActivateSM: CanActivateFn = async ( ) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const orgs = await orgService.getAll(); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const orgs = await firstValueFrom(orgService.organizations$(userId)); const smOrg = orgs.find((o) => o.canAccessSecretsManager); if (smOrg) { return createUrlTreeFromSnapshot(route, ["/sm", smOrg.id]); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index adf01afd10c..6594b71a14c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -15,8 +15,13 @@ import { takeUntil, } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo"; import { OrganizationCounts } from "../models/view/counts.view"; @@ -41,6 +46,7 @@ export class NavigationComponent implements OnInit, OnDestroy { constructor( protected route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, private countService: CountService, private projectService: ProjectService, private secretService: SecretService, @@ -50,7 +56,15 @@ export class NavigationComponent implements OnInit, OnDestroy { ngOnInit() { const org$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get(params.organizationId)), + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), distinctUntilChanged(), takeUntil(this.destroy$), ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index a95192e0d91..cb3f6016766 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -20,8 +20,13 @@ import { import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; 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"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -112,6 +117,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private serviceAccountService: ServiceAccountService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, @@ -130,7 +136,15 @@ export class OverviewComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const org$ = orgId$.pipe(switchMap((orgId) => this.organizationService.get(orgId))); + const org$ = orgId$.pipe( + switchMap((orgId) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe(getOrganizationById(orgId)), + ), + ), + ), + ); org$.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.organizationId = org.id; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts index b9c09a0d671..94d9ab2d8ee 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; import { BannerModule } from "@bitwarden/components"; +import { OnboardingModule } from "@bitwarden/web-vault/app/shared/components/onboarding/onboarding.module"; -import { OnboardingModule } from "../../../../../../apps/web/src/app/shared/components/onboarding/onboarding.module"; import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; import { OverviewRoutingModule } from "./overview-routing.module"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts index 631f6409748..159b90d5432 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts @@ -3,13 +3,18 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { ToastService } from "@bitwarden/components"; +import { RouterService } from "@bitwarden/web-vault/app/core"; -import { RouterService } from "../../../../../../../apps/web/src/app/core/router.service"; import { ProjectView } from "../../models/view/project.view"; import { ProjectService } from "../project.service"; @@ -32,6 +37,8 @@ describe("Project Redirect Guard", () => { let i18nServiceMock: MockProxy; let toastService: MockProxy; let router: Router; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; const projectView = { @@ -50,6 +57,7 @@ describe("Project Redirect Guard", () => { projectServiceMock = mock(); i18nServiceMock = mock(); toastService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ imports: [ @@ -71,6 +79,7 @@ describe("Project Redirect Guard", () => { ], providers: [ { provide: OrganizationService, useValue: organizationService }, + { provide: AccountService, useValue: accountService }, { provide: RouterService, useValue: routerService }, { provide: ProjectService, useValue: projectServiceMock }, { provide: I18nService, useValue: i18nServiceMock }, @@ -83,7 +92,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); // Act @@ -95,7 +104,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/projects if project does not exist", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); // Act await router.navigateByUrl("sm/123/projects/124"); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index 1ab8b7e0196..d9919ef6bac 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -2,7 +2,7 @@
+ + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts new file mode 100644 index 00000000000..d6a757fe897 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts @@ -0,0 +1,35 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; + +type AppAtRiskMembersDialogParams = { + members: MemberDetailsFlat[]; + applicationName: string; +}; + +export const openAppAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AppAtRiskMembersDialogParams, +) => + dialogService.open(AppAtRiskMembersDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./app-at-risk-members-dialog.component.html", + imports: [ButtonModule, CommonModule, JslibModule, DialogModule], +}) +export class AppAtRiskMembersDialogComponent { + protected members: MemberDetailsFlat[]; + protected applicationName: string; + + constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) { + this.members = params.members; + this.applicationName = params.applicationName; + } +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts deleted file mode 100644 index 4df363ab2c7..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts +++ /dev/null @@ -1,50 +0,0 @@ -export const applicationTableMockData = [ - { - id: 1, - name: "google.com", - atRiskPasswords: 4, - totalPasswords: 10, - atRiskMembers: 2, - totalMembers: 5, - }, - { - id: 2, - name: "facebook.com", - atRiskPasswords: 3, - totalPasswords: 8, - atRiskMembers: 1, - totalMembers: 3, - }, - { - id: 3, - name: "twitter.com", - atRiskPasswords: 2, - totalPasswords: 6, - atRiskMembers: 0, - totalMembers: 2, - }, - { - id: 4, - name: "linkedin.com", - atRiskPasswords: 1, - totalPasswords: 4, - atRiskMembers: 0, - totalMembers: 1, - }, - { - id: 5, - name: "instagram.com", - atRiskPasswords: 0, - totalPasswords: 2, - atRiskMembers: 0, - totalMembers: 0, - }, - { - id: 6, - name: "tiktok.com", - atRiskPasswords: 0, - totalPasswords: 1, - atRiskMembers: 0, - totalMembers: 0, - }, -]; diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html index 1c503f3d786..41d256c0734 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html @@ -35,17 +35,19 @@

{{ "criticalApplications" | i18n }}

@@ -60,38 +62,53 @@

{{ "criticalApplications" | i18n }}

- {{ "application" | i18n }} - {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} - + - + - - {{ r.name }} + + {{ r.applicationName }} - {{ r.atRiskPasswords }} + {{ r.atRiskPasswordCount }} - {{ r.totalPasswords }} + {{ r.passwordCount }} - {{ r.atRiskMembers }} + {{ r.atRiskMemberCount }} - {{ r.totalMembers }} + {{ r.memberCount }} + + + + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts index 99f68aa9c72..84a32ffcd6d 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts @@ -4,16 +4,35 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { debounceTime, map } from "rxjs"; +import { combineLatest, debounceTime, map } from "rxjs"; +import { + CriticalAppsService, + RiskInsightsDataService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetailWithCriticalFlag, + ApplicationHealthReportSummary, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + DialogService, + Icons, + NoItemsModule, + SearchModule, + TableDataSource, + ToastService, +} from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; +import { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component"; +import { OrgAtRiskAppsDialogComponent } from "./org-at-risk-apps-dialog.component"; +import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component"; import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ @@ -21,32 +40,41 @@ import { RiskInsightsTabType } from "./risk-insights.component"; selector: "tools-critical-applications", templateUrl: "./critical-applications.component.html", imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], + providers: [], }) export class CriticalApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); protected loading = false; protected organizationId: string; + protected applicationSummary = {} as ApplicationHealthReportSummary; noItemsIcon = Icons.Security; - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; ngOnInit() { - this.activatedRoute.paramMap + this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; + combineLatest([ + this.dataService.applications$, + this.criticalAppsService.getAppsListForOrg(this.organizationId), + ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(async (params) => { - this.organizationId = params.get("organizationId"); - // TODO: use organizationId to fetch data + map(([applications, criticalApps]) => { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; + return data?.filter((app) => app.isMarkedAsCritical); }), ) - .subscribe(); + .subscribe((applications) => { + if (applications) { + this.dataSource.data = applications; + this.applicationSummary = this.reportService.generateApplicationsSummary(applications); + } + }); } goToAllAppsTab = async () => { @@ -56,14 +84,66 @@ export class CriticalApplicationsComponent implements OnInit { }); }; + unmarkAsCriticalApp = async (hostname: string) => { + try { + await this.criticalAppsService.dropCriticalApp( + this.organizationId as OrganizationId, + hostname, + ); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + return; + } + + this.toastService.showToast({ + message: this.i18nService.t("criticalApplicationSuccessfullyUnmarked"), + variant: "success", + title: this.i18nService.t("success"), + }); + this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname); + }; + constructor( - protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected router: Router, + protected toastService: ToastService, + protected dataService: RiskInsightsDataService, + protected criticalAppsService: CriticalAppsService, + protected reportService: RiskInsightsReportService, + protected dialogService: DialogService, + protected i18nService: I18nService, ) { - this.dataSource.data = []; //applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); } + + showAppAtRiskMembers = async (applicationName: string) => { + openAppAtRiskMembersDialog(this.dialogService, { + members: + this.dataSource.data.find((app) => app.applicationName === applicationName) + ?.atRiskMemberDetails ?? [], + applicationName, + }); + }; + + showOrgAtRiskMembers = async () => { + this.dialogService.open(OrgAtRiskMembersDialogComponent, { + data: this.reportService.generateAtRiskMemberList(this.dataSource.data), + }); + }; + + showOrgAtRiskApps = async () => { + this.dialogService.open(OrgAtRiskAppsDialogComponent, { + data: this.reportService.generateAtRiskApplicationList(this.dataSource.data), + }); + }; + + trackByFunction(_: number, item: ApplicationHealthReportDetailWithCriticalFlag) { + return item.applicationName; + } } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html new file mode 100644 index 00000000000..298011b2157 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html @@ -0,0 +1,25 @@ + + + {{ "atRiskApplicationsWithCount" | i18n: atRiskApps.length }} + + +
+ {{ "atRiskApplicationsDescription" | i18n }} +
+
{{ "application" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
+
+
+ + + +
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts new file mode 100644 index 00000000000..0ae00f60874 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts @@ -0,0 +1,24 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AtRiskApplicationDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; + +export const openOrgAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AtRiskApplicationDetail[], +) => + dialogService.open(OrgAtRiskAppsDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./org-at-risk-apps-dialog.component.html", + imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], +}) +export class OrgAtRiskAppsDialogComponent { + constructor(@Inject(DIALOG_DATA) protected atRiskApps: AtRiskApplicationDetail[]) {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html new file mode 100644 index 00000000000..1f1de103661 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html @@ -0,0 +1,25 @@ + + + {{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} + + +
+ {{ "atRiskMembersDescription" | i18n }} +
+
{{ "email" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
+
+
+ + + +
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts new file mode 100644 index 00000000000..72518843d94 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts @@ -0,0 +1,24 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; + +export const openOrgAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AtRiskMemberDetail[], +) => + dialogService.open(OrgAtRiskMembersDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./org-at-risk-members-dialog.component.html", + imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], +}) +export class OrgAtRiskMembersDialogComponent { + constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts index 1e9e4171bc3..852e9adafb3 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts @@ -10,8 +10,12 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; 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 { mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; @@ -24,6 +28,7 @@ describe("PasswordHealthMembersUriComponent", () => { let fixture: ComponentFixture; let cipherServiceMock: MockProxy; const passwordHealthServiceMock = mock(); + const userId = Utils.newGuid() as UserId; const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); @@ -36,6 +41,7 @@ describe("PasswordHealthMembersUriComponent", () => { { provide: I18nService, useValue: mock() }, { provide: AuditService, useValue: mock() }, { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: mockAccountServiceWith(userId) }, { provide: PasswordStrengthServiceAbstraction, useValue: mock(), diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 7fe320ede6a..ae8bd94e5f3 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -40,7 +40,7 @@

{{ "riskInsights" | i18n }}

- {{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }} + {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 75601994c70..6d39a710e24 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -2,15 +2,23 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { Observable, EMPTY } from "rxjs"; +import { EMPTY, Observable } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { + CriticalAppsService, + RiskInsightsDataService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetail, + PasswordHealthReportApplicationsResponse, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +// eslint-disable-next-line no-restricted-imports -- used for dependency injection import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; @@ -51,6 +59,7 @@ export class RiskInsightsComponent implements OnInit { dataLastUpdated: Date = new Date(); isCriticalAppsFeatureEnabled: boolean = false; + criticalApps$: Observable = new Observable(); showDebugTabs: boolean = false; appsCount: number = 0; @@ -69,10 +78,13 @@ export class RiskInsightsComponent implements OnInit { private router: Router, private configService: ConfigService, private dataService: RiskInsightsDataService, + private criticalAppsService: CriticalAppsService, ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; }); + const orgId = this.route.snapshot.paramMap.get("organizationId") ?? ""; + this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(orgId); } async ngOnInit() { @@ -104,6 +116,7 @@ export class RiskInsightsComponent implements OnInit { if (applications) { this.appsCount = applications.length; } + this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId); }, }); } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index 6aaaf1a4066..52ec2901031 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -93,17 +93,16 @@ export class MemberAccessReportComponent implements OnInit { }); }; - edit = async (user: MemberAccessReportView | null): Promise => { + edit = async (user: MemberAccessReportView): Promise => { const dialog = openUserAddEditDialog(this.dialogService, { data: { + kind: "Edit", name: this.userNamePipe.transform(user), organizationId: this.organizationId, - organizationUserId: user != null ? user.userGuid : null, - allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], - usesKeyConnector: user?.usesKeyConnector, + organizationUserId: user.userGuid, + usesKeyConnector: user.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: MemberDialogTab.Role, - numConfirmedMembers: this.dataSource.data.length, }, }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts index 9919da8d08e..cf2f3b6417b 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { OrganizationId } from "@bitwarden/common/src/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessReportView } from "../view/member-access-report.view"; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 6aab54f77d5..7d6beca48ec 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { OrganizationId } from "@bitwarden/common/src/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; import { memberAccessReportsMock } from "./member-access-report.mock"; diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index c4304ec2bd9..13a6466b3b5 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -26,6 +26,7 @@ "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/platform": ["../../libs/platform/src"], + "@bitwarden/ui-common": ["../../libs/ui/common/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/vault": ["../../libs/vault/src"], diff --git a/libs/admin-console/jest.config.js b/libs/admin-console/jest.config.js index f2a8e6458af..5131753964c 100644 --- a/libs/admin-console/jest.config.js +++ b/libs/admin-console/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts index 4aa54429aad..048a4733948 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts @@ -91,7 +91,7 @@ describe("DefaultvNextCollectionService", () => { // Assert emitted values expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: "DEC_NAME_" + collection1.id, @@ -167,7 +167,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("ENC_NAME_" + collection1.id), @@ -205,7 +205,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(3); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("UPDATED_ENC_NAME_" + collection1.id), @@ -230,7 +230,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(1); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("ENC_NAME_" + collection1.id), @@ -253,7 +253,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(1); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: newCollection3.id, name: makeEncString("ENC_NAME_" + newCollection3.id), diff --git a/libs/admin-console/test.setup.ts b/libs/admin-console/test.setup.ts index 6be6e7b8dd1..8ab102f2cf4 100644 --- a/libs/admin-console/test.setup.ts +++ b/libs/admin-console/test.setup.ts @@ -1,6 +1,10 @@ import { webcrypto } from "crypto"; + +import { addCustomMatchers } from "@bitwarden/common/spec"; import "jest-preset-angular/setup-jest"; +addCustomMatchers(); + Object.defineProperty(window, "CSS", { value: null }); Object.defineProperty(window, "getComputedStyle", { value: () => { diff --git a/libs/admin-console/tsconfig.json b/libs/admin-console/tsconfig.json index 6004a56fb55..4f057fd6af0 100644 --- a/libs/admin-console/tsconfig.json +++ b/libs/admin-console/tsconfig.json @@ -1,5 +1,13 @@ { - "extends": "../shared/tsconfig.libs", - "include": ["src", "spec"], + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/key-management": ["../key-management/src"] + } + }, + "include": ["src", "spec", "../../libs/common/custom-matchers.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index c8e748575c0..5e73614eb8e 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 0b19935985a..52a22ac2946 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -63,7 +63,15 @@ export class CollectionsComponent implements OnInit { } if (this.organization == null) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(activeUserId) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.cipher.organizationId), + ), + ), + ); } } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts b/libs/angular/src/auth/components/authentication-timeout.component.ts similarity index 89% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts rename to libs/angular/src/auth/components/authentication-timeout.component.ts index faa08cf073b..1a5d398a291 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts +++ b/libs/angular/src/auth/components/authentication-timeout.component.ts @@ -10,7 +10,7 @@ import { ButtonModule } from "@bitwarden/components"; * It provides a button to navigate to the login page. */ @Component({ - selector: "app-two-factor-expired", + selector: "app-authentication-timeout", standalone: true, imports: [CommonModule, JslibModule, ButtonModule, RouterModule], template: ` @@ -22,4 +22,4 @@ import { ButtonModule } from "@bitwarden/components"; `, }) -export class TwoFactorTimeoutComponent {} +export class AuthenticationTimeoutComponent {} diff --git a/libs/angular/src/auth/components/login-v1.component.ts b/libs/angular/src/auth/components/login-v1.component.ts index 0775114bd8c..3416901da97 100644 --- a/libs/angular/src/auth/components/login-v1.component.ts +++ b/libs/angular/src/auth/components/login-v1.component.ts @@ -10,11 +10,9 @@ import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, PasswordLoginCredentials, - RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -56,7 +54,7 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni return this.formGroup.controls.email; } - formGroup = this.formBuilder.group({ + formGroup = this.formBuilder.nonNullable.group({ email: ["", [Validators.required, Validators.email]], masterPassword: [ "", @@ -67,14 +65,12 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni protected twoFactorRoute = "2fa"; protected successRoute = "vault"; - // TODO: remove when email verification flag is removed - protected registerRoute$ = this.registerRouteService.registerRoute$(); protected forcePasswordResetRoute = "update-temp-password"; protected destroy$ = new Subject(); get loggedEmail() { - return this.formGroup.value.email; + return this.formGroup.controls.email.value; } constructor( @@ -95,8 +91,6 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni protected route: ActivatedRoute, protected loginEmailService: LoginEmailServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, - protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, - protected registerRouteService: RegisterRouteService, protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService, toastService); @@ -146,8 +140,6 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni } async submit(showToast = true) { - const data = this.formGroup.value; - await this.setupCaptcha(); this.formGroup.markAllAsTouched(); @@ -170,10 +162,10 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni try { const credentials = new PasswordLoginCredentials( - data.email, - data.masterPassword, + this.formGroup.controls.email.value, + this.formGroup.controls.masterPassword.value, this.captchaToken, - null, + undefined, ); this.formPromise = this.loginStrategyService.logIn(credentials); @@ -397,6 +389,8 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni email, deviceIdentifier, ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.showLoginWithDevice = false; } diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 8b4df78175f..6c13809566a 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -19,7 +19,6 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -50,7 +49,7 @@ export class SsoComponent implements OnInit { protected twoFactorRoute = "2fa"; protected successRoute = "lock"; protected trustedDeviceEncRoute = "login-initiated"; - protected changePasswordRoute = "set-password"; + protected changePasswordRoute = "set-password-jit"; protected forcePasswordResetRoute = "update-temp-password"; protected clientId: string; protected redirectUri: string; @@ -339,14 +338,6 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - if (emailVerification) { - this.changePasswordRoute = "set-password-jit"; - } - await this.navigateViaCallbackOrRoute( this.onSuccessfulLoginChangePasswordNavigate, [this.changePasswordRoute], diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 5a1903d6671..414aa1dc2a3 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -86,12 +86,12 @@ describe("TwoFactorComponent", () => { }; let selectedUserDecryptionOptions: BehaviorSubject; - let twoFactorTimeoutSubject: BehaviorSubject; + let authenticationSessionTimeoutSubject: BehaviorSubject; beforeEach(() => { - twoFactorTimeoutSubject = new BehaviorSubject(false); + authenticationSessionTimeoutSubject = new BehaviorSubject(false); mockLoginStrategyService = mock(); - mockLoginStrategyService.twoFactorTimeout$ = twoFactorTimeoutSubject; + mockLoginStrategyService.authenticationSessionTimeout$ = authenticationSessionTimeoutSubject; mockRouter = mock(); mockI18nService = mock(); mockApiService = mock(); @@ -153,7 +153,9 @@ describe("TwoFactorComponent", () => { }), }; - selectedUserDecryptionOptions = new BehaviorSubject(null); + selectedUserDecryptionOptions = new BehaviorSubject( + mockUserDecryptionOpts.withMasterPassword, + ); mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; TestBed.configureTestingModule({ @@ -497,8 +499,8 @@ describe("TwoFactorComponent", () => { }); it("navigates to the timeout route when timeout expires", async () => { - twoFactorTimeoutSubject.next(true); + authenticationSessionTimeoutSubject.next(true); - expect(mockRouter.navigate).toHaveBeenCalledWith(["2fa-timeout"]); + expect(mockRouter.navigate).toHaveBeenCalledWith(["authentication-timeout"]); }); }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 18bfe546600..3b3459f42fb 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -71,7 +71,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; - protected twoFactorTimeoutRoute = "2fa-timeout"; + protected twoFactorTimeoutRoute = "authentication-timeout"; get isDuoProvider(): boolean { return ( @@ -104,8 +104,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); - // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired - this.loginStrategyService.twoFactorTimeout$ + // Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired + this.loginStrategyService.authenticationSessionTimeout$ .pipe(takeUntilDestroyed()) .subscribe(async (expired) => { if (!expired) { @@ -157,7 +157,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), - message: error, + message: this.i18nService.t("webauthnCancelOrTimeout"), }); }, (info: string) => { diff --git a/libs/angular/src/auth/guards/active-auth.guard.spec.ts b/libs/angular/src/auth/guards/active-auth.guard.spec.ts new file mode 100644 index 00000000000..c3417b9d41d --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.spec.ts @@ -0,0 +1,71 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { activeAuthGuard } from "./active-auth.guard"; + +@Component({ template: "" }) +class EmptyComponent {} + +describe("activeAuthGuard", () => { + const setup = (authType: AuthenticationType | null) => { + const loginStrategyService: MockProxy = + mock(); + const currentAuthTypeSubject = new BehaviorSubject(authType); + loginStrategyService.currentAuthType$ = currentAuthTypeSubject; + + const logService: MockProxy = mock(); + + const testBed = TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: "", component: EmptyComponent }, + { + path: "protected-route", + component: EmptyComponent, + canActivate: [activeAuthGuard()], + }, + { path: "login", component: EmptyComponent }, + ]), + ], + providers: [ + { provide: LoginStrategyServiceAbstraction, useValue: loginStrategyService }, + { provide: LogService, useValue: logService }, + ], + declarations: [EmptyComponent], + }); + + return { + router: testBed.inject(Router), + logService, + loginStrategyService, + }; + }; + + it("creates the guard", () => { + const { router } = setup(AuthenticationType.Password); + expect(router).toBeTruthy(); + }); + + it("allows access with an active login session", async () => { + const { router } = setup(AuthenticationType.Password); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/protected-route"); + }); + + it("redirects to login with no active session", async () => { + const { router, logService } = setup(null); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/login"); + expect(logService.error).toHaveBeenCalledWith("No active login session found."); + }); +}); diff --git a/libs/angular/src/auth/guards/active-auth.guard.ts b/libs/angular/src/auth/guards/active-auth.guard.ts new file mode 100644 index 00000000000..56213bbd979 --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.ts @@ -0,0 +1,28 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +/** + * Guard that ensures there is an active login session before allowing access + * to the new device verification route. + * If not, redirects to login. + */ +export function activeAuthGuard(): CanActivateFn { + return async () => { + const loginStrategyService = inject(LoginStrategyServiceAbstraction); + const logService = inject(LogService); + const router = inject(Router); + + // Check if we have a valid login session + const authType = await firstValueFrom(loginStrategyService.currentAuthType$); + if (authType === null) { + logService.error("No active login session found."); + return router.createUrlTree(["/login"]); + } + + return true; + }; +} diff --git a/libs/angular/src/auth/guards/index.ts b/libs/angular/src/auth/guards/index.ts index 1760a870b3a..026848c4b08 100644 --- a/libs/angular/src/auth/guards/index.ts +++ b/libs/angular/src/auth/guards/index.ts @@ -1,4 +1,5 @@ export * from "./auth.guard"; +export * from "./active-auth.guard"; export * from "./lock.guard"; export * from "./redirect.guard"; export * from "./tde-decryption-required.guard"; diff --git a/libs/angular/src/auth/guards/unauth.guard.spec.ts b/libs/angular/src/auth/guards/unauth.guard.spec.ts index 6d8619f4d43..ec36b146a03 100644 --- a/libs/angular/src/auth/guards/unauth.guard.spec.ts +++ b/libs/angular/src/auth/guards/unauth.guard.spec.ts @@ -5,17 +5,48 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { unauthGuardFn } from "./unauth.guard"; describe("UnauthGuard", () => { - const setup = (authStatus: AuthenticationStatus) => { + const activeUser: Account = { + id: "fake_user_id" as UserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }; + + const setup = ( + activeUser: Account | null, + authStatus: AuthenticationStatus | null = null, + tdeEnabled: boolean = false, + everHadUserKey: boolean = false, + ) => { + const accountService: MockProxy = mock(); const authService: MockProxy = mock(); - authService.getAuthStatus.mockResolvedValue(authStatus); - const activeAccountStatusObservable = new BehaviorSubject(authStatus); - authService.activeAccountStatus$ = activeAccountStatusObservable; + const keyService: MockProxy = mock(); + const deviceTrustService: MockProxy = + mock(); + const logService: MockProxy = mock(); + + accountService.activeAccount$ = new BehaviorSubject(activeUser); + + if (authStatus !== null) { + const activeAccountStatusObservable = new BehaviorSubject(authStatus); + authService.authStatusFor$.mockReturnValue(activeAccountStatusObservable); + } + + keyService.everHadUserKey$ = new BehaviorSubject(everHadUserKey); + deviceTrustService.supportsDeviceTrustByUserId$.mockReturnValue( + new BehaviorSubject(tdeEnabled), + ); const testBed = TestBed.configureTestingModule({ imports: [ @@ -30,6 +61,7 @@ describe("UnauthGuard", () => { { path: "lock", component: EmptyComponent }, { path: "testhomepage", component: EmptyComponent }, { path: "testlocked", component: EmptyComponent }, + { path: "login-initiated", component: EmptyComponent }, { path: "testOverrides", component: EmptyComponent, @@ -39,7 +71,13 @@ describe("UnauthGuard", () => { }, ]), ], - providers: [{ provide: AuthService, useValue: authService }], + providers: [ + { provide: AccountService, useValue: accountService }, + { provide: AuthService, useValue: authService }, + { provide: KeyService, useValue: keyService }, + { provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService }, + { provide: LogService, useValue: logService }, + ], }); return { @@ -48,40 +86,54 @@ describe("UnauthGuard", () => { }; it("should be created", () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); expect(router).toBeTruthy(); }); it("should redirect to /vault for guarded routes when logged in and unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/vault"); }); + it("should allow access to guarded routes when account is null", async () => { + const { router } = setup(null); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/unauth-guarded-route"); + }); + it("should allow access to guarded routes when logged out", async () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/unauth-guarded-route"); }); + it("should redirect to /login-initiated when locked, TDE is enabled, and the user hasn't decrypted yet", async () => { + const { router } = setup(activeUser, AuthenticationStatus.Locked, true, false); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/login-initiated"); + }); + it("should redirect to /lock for guarded routes when locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/lock"); }); it("should redirect to /testhomepage for guarded routes when testOverrides are provided and the account is unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testhomepage"); }); it("should redirect to /testlocked for guarded routes when testOverrides are provided and the account is locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testlocked"); diff --git a/libs/angular/src/auth/guards/unauth.guard.ts b/libs/angular/src/auth/guards/unauth.guard.ts index f96668773ef..1ac0eebb458 100644 --- a/libs/angular/src/auth/guards/unauth.guard.ts +++ b/libs/angular/src/auth/guards/unauth.guard.ts @@ -1,9 +1,13 @@ import { inject } from "@angular/core"; -import { CanActivateFn, Router, UrlTree } from "@angular/router"; -import { Observable, map } from "rxjs"; +import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KeyService } from "@bitwarden/key-management"; type UnauthRoutes = { homepage: () => string; @@ -15,23 +19,54 @@ const defaultRoutes: UnauthRoutes = { locked: "/lock", }; -function unauthGuard(routes: UnauthRoutes): Observable { +// TODO: PM-17195 - Investigate consolidating unauthGuard and redirectGuard into AuthStatusGuard +async function unauthGuard( + route: ActivatedRouteSnapshot, + routes: UnauthRoutes, +): Promise { + const accountService = inject(AccountService); const authService = inject(AuthService); const router = inject(Router); + const keyService = inject(KeyService); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); + const logService = inject(LogService); - return authService.activeAccountStatus$.pipe( - map((status) => { - if (status == null || status === AuthenticationStatus.LoggedOut) { - return true; - } else if (status === AuthenticationStatus.Locked) { - return router.createUrlTree([routes.locked]); - } else { - return router.createUrlTree([routes.homepage()]); - } - }), + const activeUser = await firstValueFrom(accountService.activeAccount$); + + if (!activeUser) { + return true; + } + + const authStatus = await firstValueFrom(authService.authStatusFor$(activeUser.id)); + + if (authStatus == null || authStatus === AuthenticationStatus.LoggedOut) { + return true; + } + + if (authStatus === AuthenticationStatus.Unlocked) { + return router.createUrlTree([routes.homepage()]); + } + + const tdeEnabled = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(activeUser.id), ); + const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$); + + // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the + // login decryption options component. + if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { + logService.info( + "Sending user to TDE decryption options. AuthStatus is %s. TDE support is %s. Ever had user key is %s.", + AuthenticationStatus[authStatus], + tdeEnabled, + everHadUserKey, + ); + return router.createUrlTree(["/login-initiated"]); + } + + return router.createUrlTree([routes.locked]); } export function unauthGuardFn(overrides: Partial = {}): CanActivateFn { - return () => unauthGuard({ ...defaultRoutes, ...overrides }); + return async (route) => unauthGuard(route, { ...defaultRoutes, ...overrides }); } diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts index cebd81846c1..edb233cc76e 100644 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -3,14 +3,14 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -105,7 +105,16 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 20.0, }); - this.organization = await this.organizationService.get(this.dialogParams.organizationId); + this.user = await firstValueFrom(this.accountService.activeAccount$); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(this.user.id) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.dialogParams.organizationId), + ), + ), + ); payPalCustomField = "organization_id:" + this.organization.id; this.payPalConfig.subject = this.organization.name; } else if (this.dialogParams.providerId) { @@ -119,7 +128,6 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 10.0, }); - this.user = await firstValueFrom(this.accountService.activeAccount$); payPalCustomField = "user_id:" + this.user.id; this.payPalConfig.subject = this.user.email; } diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts index 13a6d2d0cc3..885afb1ae67 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -72,6 +72,10 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { } } + markAllAsTouched() { + this.formGroup.markAllAsTouched(); + } + async ngOnInit() { if (this.startWith) { this.formGroup.controls.country.setValue(this.startWith.country); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 37dec53b9c7..534a1337eda 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -54,7 +54,11 @@ export class ShareComponent implements OnInit, OnDestroy { const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); - this.organizations$ = this.organizationService.memberOrganizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + + this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe( map((orgs) => { return orgs .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 4f5a8f6673c..b06faacef19 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -25,15 +25,15 @@ import { TableModule, ToastModule, TypographyModule, + CopyClickDirective, + A11yTitleDirective, } from "@bitwarden/components"; import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; import { DeprecatedCalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; -import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; import { BoxRowDirective } from "./directives/box-row.directive"; -import { CopyClickDirective } from "./directives/copy-click.directive"; import { CopyTextDirective } from "./directives/copy-text.directive"; import { FallbackSrcDirective } from "./directives/fallback-src.directive"; import { IfFeatureDirective } from "./directives/if-feature.directive"; @@ -83,10 +83,11 @@ import { IconComponent } from "./vault/components/icon.component"; LinkModule, IconModule, TextDragDirective, + CopyClickDirective, + A11yTitleDirective, ], declarations: [ A11yInvalidDirective, - A11yTitleDirective, ApiActionDirective, AutofocusDirective, BoxRowDirective, @@ -105,7 +106,6 @@ import { IconComponent } from "./vault/components/icon.component"; StopClickDirective, StopPropDirective, TrueFalseValueDirective, - CopyClickDirective, LaunchClickDirective, UserNamePipe, PasswordStrengthComponent, diff --git a/libs/angular/src/platform/pipes/i18n.pipe.ts b/libs/angular/src/platform/pipes/i18n.pipe.ts index 1f92bbb19a4..a6fdbc78255 100644 --- a/libs/angular/src/platform/pipes/i18n.pipe.ts +++ b/libs/angular/src/platform/pipes/i18n.pipe.ts @@ -2,6 +2,9 @@ import { Pipe, PipeTransform } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +/** + * @deprecated: Please use the I18nPipe from @bitwarden/ui-common + */ @Pipe({ name: "i18n", }) diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index e7547f9b828..82b5affcc22 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -1,138 +1,4 @@ -import { Provider } from "@angular/core"; -import { Constructor, Opaque } from "type-fest"; - -import { SafeInjectionToken } from "../../services/injection-tokens"; - /** - * The return type of the {@link safeProvider} helper function. - * Used to distinguish a type safe provider definition from a non-type safe provider definition. + * @deprecated: Please use the SafeProvider & safeProvider from @bitwarden/ui-common */ -export type SafeProvider = Opaque; - -// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 -type AbstractConstructor = abstract new (...args: any) => T; - -type MapParametersToDeps = { - [K in keyof T]: AbstractConstructor | SafeInjectionToken; -}; - -type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; - -/** - * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken - */ -type ProviderInstanceType = - T extends SafeInjectionToken - ? InstanceType> - : T extends Constructor | AbstractConstructor - ? InstanceType - : never; - -/** - * Represents a dependency provided with the useClass option. - */ -type SafeClassProvider< - A extends AbstractConstructor | SafeInjectionToken, - I extends Constructor>, - D extends MapParametersToDeps>, -> = { - provide: A; - useClass: I; - deps: D; -}; - -/** - * Represents a dependency provided with the useValue option. - */ -type SafeValueProvider, V extends SafeInjectionTokenType> = { - provide: A; - useValue: V; -}; - -/** - * Represents a dependency provided with the useFactory option. - */ -type SafeFactoryProvider< - A extends AbstractConstructor | SafeInjectionToken, - I extends (...args: any) => ProviderInstanceType, - D extends MapParametersToDeps>, -> = { - provide: A; - useFactory: I; - deps: D; - multi?: boolean; -}; - -/** - * Represents a dependency provided with the useExisting option. - */ -type SafeExistingProvider< - A extends Constructor | AbstractConstructor | SafeInjectionToken, - I extends Constructor> | AbstractConstructor>, -> = { - provide: A; - useExisting: I; -}; - -/** - * Represents a dependency where there is no abstract token, the token is the implementation - */ -type SafeConcreteProvider< - I extends Constructor, - D extends MapParametersToDeps>, -> = { - provide: I; - deps: D; -}; - -/** - * If useAngularDecorators: true is specified, do not require a deps array. - * This is a manual override for where @Injectable decorators are used - */ -type UseAngularDecorators = Omit & { - useAngularDecorators: true; -}; - -/** - * Represents a type with a deps array that may optionally be overridden with useAngularDecorators - */ -type AllowAngularDecorators = T | UseAngularDecorators; - -/** - * A factory function that creates a provider for the ngModule providers array. - * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. - * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, - * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. - * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) - * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) - * @returns The exact same object without modification (pass-through). - */ -export const safeProvider = < - // types for useClass - AClass extends AbstractConstructor | SafeInjectionToken, - IClass extends Constructor>, - DClass extends MapParametersToDeps>, - // types for useValue - AValue extends SafeInjectionToken, - VValue extends SafeInjectionTokenType, - // types for useFactory - AFactory extends AbstractConstructor | SafeInjectionToken, - IFactory extends (...args: any) => ProviderInstanceType, - DFactory extends MapParametersToDeps>, - // types for useExisting - AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, - IExisting extends - | Constructor> - | AbstractConstructor>, - // types for no token - IConcrete extends Constructor, - DConcrete extends MapParametersToDeps>, ->( - provider: - | AllowAngularDecorators> - | SafeValueProvider - | AllowAngularDecorators> - | SafeExistingProvider - | AllowAngularDecorators> - | Constructor, -): SafeProvider => provider as SafeProvider; +export { SafeProvider, safeProvider } from "@bitwarden/ui-common"; diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 3842c3250e1..2c740d5bb42 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -14,17 +13,9 @@ import { Theme } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; - -declare const tag: unique symbol; -/** - * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. - * @remarks The default angular implementation does not use the generic type to define the structure of the object, - * so the structural type system will not complain about a mismatch in the type parameter. - * This is solved by assigning T to an arbitrary private property. - */ -export class SafeInjectionToken extends InjectionToken { - private readonly [tag]: T; -} +import { SafeInjectionToken } from "@bitwarden/ui-common"; +// Re-export the SafeInjectionToken from ui-common +export { SafeInjectionToken } from "@bitwarden/ui-common"; export const WINDOW = new SafeInjectionToken("WINDOW"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index f5940b8e144..50095e55400 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -20,6 +20,7 @@ import { DefaultLoginComponentService, LoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService, + DefaultLoginApprovalComponentService, } from "@bitwarden/auth/angular"; import { AuthRequestServiceAbstraction, @@ -34,11 +35,13 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, LogoutReason, - RegisterRouteService, AuthRequestApiService, DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, LoginSuccessHandlerService, + PasswordLoginStrategy, + PasswordLoginStrategyData, + LoginApprovalComponentServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -66,8 +69,8 @@ import { } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service"; import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service"; import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service"; @@ -291,6 +294,7 @@ import { UserAsymmetricKeysRegenerationApiService, DefaultUserAsymmetricKeysRegenerationApiService, } from "@bitwarden/key-management"; +import { SafeInjectionToken } from "@bitwarden/ui-common"; import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultExportService, @@ -301,6 +305,8 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; @@ -319,7 +325,6 @@ import { MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, - SafeInjectionToken, SECURE_STORAGE, STATE_FACTORY, SUPPORTS_SECURE_STORAGE, @@ -989,13 +994,14 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: InternalOrganizationServiceAbstraction, - useClass: OrganizationService, + useClass: DefaultOrganizationService, deps: [StateProvider], }), safeProvider({ provide: OrganizationServiceAbstraction, useExisting: InternalOrganizationServiceAbstraction, }), + safeProvider({ provide: OrganizationUserApiService, useClass: DefaultOrganizationUserApiService, @@ -1230,7 +1236,6 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, BillingApiServiceAbstraction, - ConfigService, KeyService, EncryptService, I18nServiceAbstraction, @@ -1350,11 +1355,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultServerSettingsService, deps: [ConfigService], }), - safeProvider({ - provide: RegisterRouteService, - useClass: RegisterRouteService, - deps: [ConfigService], - }), safeProvider({ provide: AnonLayoutWrapperDataService, useClass: DefaultAnonLayoutWrapperDataService, @@ -1396,13 +1396,18 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherAuthorizationService, useClass: DefaultCipherAuthorizationService, - deps: [CollectionService, OrganizationServiceAbstraction], + deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], }), safeProvider({ provide: AuthRequestApiService, useClass: DefaultAuthRequestApiService, deps: [ApiServiceAbstraction, LogService], }), + safeProvider({ + provide: LoginApprovalComponentServiceAbstraction, + useClass: DefaultLoginApprovalComponentService, + deps: [], + }), safeProvider({ provide: LoginDecryptionOptionsService, useClass: DefaultLoginDecryptionOptionsService, @@ -1432,6 +1437,37 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginSuccessHandlerService, deps: [SyncService, UserAsymmetricKeysRegenerationService], }), + safeProvider({ + provide: PasswordLoginStrategy, + useClass: PasswordLoginStrategy, + deps: [ + PasswordLoginStrategyData, + PasswordStrengthServiceAbstraction, + PolicyServiceAbstraction, + LoginStrategyServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + KeyService, + EncryptService, + ApiServiceAbstraction, + TokenServiceAbstraction, + AppIdServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + LogService, + StateServiceAbstraction, + TwoFactorServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, + BillingAccountProfileStateService, + VaultTimeoutSettingsServiceAbstraction, + KdfConfigService, + ], + }), + safeProvider({ + provide: PasswordLoginStrategyData, + useClass: PasswordLoginStrategyData, + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index aeee1fa104c..4f7d4b6b600 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -16,6 +16,7 @@ import { 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -164,9 +165,10 @@ export class AddEditComponent implements OnInit, OnDestroy { } }); - this.policyService - .getAll$(PolicyType.SendOptions) + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntil(this.destroy$), ) diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 8d286e0a3f9..26f645d89ef 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -7,10 +7,7 @@ import { concatMap, firstValueFrom, map, Observable, Subject, takeUntil } from " import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -128,7 +125,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected logService: LogService, protected passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService, + protected organizationService: OrganizationService, protected dialogService: DialogService, protected win: Window, protected datePipe: DatePipe, @@ -235,9 +232,12 @@ export class AddEditComponent implements OnInit, OnDestroy { this.ownershipOptions.push({ name: myEmail, value: null }); } - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); orgs - .filter(isMember) + .filter((org) => org.isMember) .sort(Utils.getSortFunction(this.i18nService, "name")) .forEach((o) => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { @@ -313,10 +313,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } // Only Admins can clone a cipher to different owner if (this.cloneMode && this.cipher.organizationId != null) { - const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find( - (o) => o.id === this.cipher.organizationId, + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const cipherOrg = ( + await firstValueFrom(this.organizationService.memberOrganizations$(activeUserId)) + ).find((o) => o.id === this.cipher.organizationId); + if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) { this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }]; } @@ -372,11 +376,11 @@ export class AddEditComponent implements OnInit, OnDestroy { } if (this.cipher.name == null || this.cipher.name === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nameRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nameRequired"), + }); return false; } @@ -385,11 +389,11 @@ export class AddEditComponent implements OnInit, OnDestroy { !this.allowPersonal && this.cipher.organizationId == null ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("personalOwnershipSubmitError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("personalOwnershipSubmitError"), + }); return false; } @@ -424,11 +428,11 @@ export class AddEditComponent implements OnInit, OnDestroy { this.formPromise = this.saveCipher(cipher); await this.formPromise; this.cipher.id = cipher.id; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"), + }); this.onSavedCipher.emit(this.cipher); this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher"); return true; @@ -514,11 +518,13 @@ export class AddEditComponent implements OnInit, OnDestroy { try { this.deletePromise = this.deleteCipher(); await this.deletePromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); this.onDeletedCipher.emit(this.cipher); this.messagingService.send( this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher", @@ -538,7 +544,11 @@ export class AddEditComponent implements OnInit, OnDestroy { try { this.restorePromise = this.restoreCipher(); await this.restorePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); this.onRestoredCipher.emit(this.cipher); this.messagingService.send("restoredCipher"); } catch (e) { @@ -652,7 +662,13 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.collections.length === 1) { (this.collections[0] as any).checked = true; } - const org = await this.organizationService.get(this.cipher.organizationId); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const org = ( + await firstValueFrom(this.organizationService.organizations$(activeUserId)) + ).find((org) => org.id === this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; } @@ -679,13 +695,17 @@ export class AddEditComponent implements OnInit, OnDestroy { this.checkPasswordPromise = null; if (matches > 0) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("passwordExposed", matches.toString()), - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("passwordExposed", matches.toString()), + }); } else { - this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordSafe"), + }); } } @@ -779,11 +799,11 @@ export class AddEditComponent implements OnInit, OnDestroy { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (typeI18nKey === "password") { void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [ diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 425b4be2840..a3b635f151d 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -64,21 +64,21 @@ export class AttachmentsComponent implements OnInit { const fileEl = document.getElementById("file") as HTMLInputElement; const files = fileEl.files; if (files == null || files.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("selectFile"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); return; } if (files[0].size > 524288000) { // 500 MB - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("maxFileSize"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("maxFileSize"), + }); return; } @@ -91,7 +91,11 @@ export class AttachmentsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), ); - this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); this.onUploadedAttachment.emit(); } catch (e) { this.logService.error(e); @@ -122,7 +126,11 @@ export class AttachmentsComponent implements OnInit { try { this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); await this.deletePromises[attachment.id]; - this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedAttachment")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedAttachment"), + }); const i = this.cipher.attachments.indexOf(attachment); if (i > -1) { this.cipher.attachments.splice(i, 1); @@ -142,11 +150,11 @@ export class AttachmentsComponent implements OnInit { } if (!this.canAccessAttachments) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("premiumRequired"), - this.i18nService.t("premiumRequiredDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); return; } @@ -171,7 +179,11 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -192,8 +204,14 @@ export class AttachmentsComponent implements OnInit { title: null, message: this.i18nService.t("fileSavedToDevice"), }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; @@ -241,7 +259,11 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(attachment.url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -279,14 +301,20 @@ export class AttachmentsComponent implements OnInit { } } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("attachmentSaved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); this.onReuploadedAttachment.emit(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; diff --git a/libs/angular/src/vault/components/folder-add-edit.component.ts b/libs/angular/src/vault/components/folder-add-edit.component.ts index 205733ba48d..28ed0dc2aed 100644 --- a/libs/angular/src/vault/components/folder-add-edit.component.ts +++ b/libs/angular/src/vault/components/folder-add-edit.component.ts @@ -11,7 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; 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"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @Directive() @@ -43,6 +43,7 @@ export class FolderAddEditComponent implements OnInit { protected logService: LogService, protected dialogService: DialogService, protected formBuilder: FormBuilder, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -52,11 +53,11 @@ export class FolderAddEditComponent implements OnInit { async submit(): Promise { this.folder.name = this.formGroup.controls.name.value; if (this.folder.name == null || this.folder.name === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nameRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nameRequired"), + }); return false; } @@ -66,11 +67,11 @@ export class FolderAddEditComponent implements OnInit { const folder = await this.folderService.encrypt(this.folder, userKey); this.formPromise = this.folderApiService.save(folder, activeUserId); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), + }); this.onSavedFolder.emit(this.folder); return true; } catch (e) { @@ -95,7 +96,11 @@ export class FolderAddEditComponent implements OnInit { const activeUserId = await firstValueFrom(this.activeUserId$); this.deletePromise = this.folderApiService.delete(this.folder.id, activeUserId); await this.deletePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedFolder")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedFolder"), + }); this.onDeletedFolder.emit(this.folder); } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html index 976c6ea421d..f16545617c9 100644 --- a/libs/angular/src/vault/components/icon.component.html +++ b/libs/angular/src/vault/components/icon.component.html @@ -4,13 +4,13 @@ [src]="data.image" [appFallbackSrc]="data.fallbackImage" *ngIf="data.imageEnabled && data.image" - class="tw-max-h-6 tw-max-w-6 tw-rounded-md" + class="tw-h-6 tw-w-6 tw-rounded-md" alt="" decoding="async" loading="lazy" />
diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 942a34c58bb..0b385688d0b 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; +import { ToastService } from "@bitwarden/components"; @Directive() export class PasswordHistoryComponent implements OnInit { @@ -20,6 +21,7 @@ export class PasswordHistoryComponent implements OnInit { protected i18nService: I18nService, protected accountService: AccountService, private win: Window, + private toastService: ToastService, ) {} async ngOnInit() { @@ -29,11 +31,11 @@ export class PasswordHistoryComponent implements OnInit { copy(password: string) { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t("password")), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("password")), + }); } protected async init() { diff --git a/libs/angular/src/vault/components/premium.component.ts b/libs/angular/src/vault/components/premium.component.ts index 8b1f215ef42..e86c6beda47 100644 --- a/libs/angular/src/vault/components/premium.component.ts +++ b/libs/angular/src/vault/components/premium.component.ts @@ -12,7 +12,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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 { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; @Directive() export class PremiumComponent implements OnInit { @@ -31,6 +31,7 @@ export class PremiumComponent implements OnInit { protected dialogService: DialogService, private environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, accountService: AccountService, ) { this.isPremium$ = accountService.activeAccount$.pipe( @@ -51,7 +52,11 @@ export class PremiumComponent implements OnInit { try { this.refreshPromise = this.apiService.refreshIdentityToken(); await this.refreshPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("refreshComplete"), + }); } catch (e) { this.logService.error(e); } diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 052ab95f3bc..4ef00e90063 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs"; +import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -118,6 +118,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected async doSearch(indexedCiphers?: CipherView[]) { indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); + + const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); + if (failedCiphers != null && failedCiphers.length > 0) { + indexedCiphers = [...failedCiphers, ...indexedCiphers]; + } + this.ciphers = await this.searchService.searchCiphers( this.searchText, [this.filter, this.deletedFilter], diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index fc12aeff2f2..18caa875e03 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -40,7 +40,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -65,6 +65,7 @@ export class ViewComponent implements OnDestroy, OnInit { showPrivateKey: boolean; canAccessPremium: boolean; showPremiumRequiredTotp: boolean; + showUpgradeRequiredTotp: boolean; totpCode: string; totpCodeFormatted: string; totpDash: number; @@ -114,6 +115,7 @@ export class ViewComponent implements OnDestroy, OnInit { protected datePipe: DatePipe, protected accountService: AccountService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected toastService: ToastService, private cipherAuthorizationService: CipherAuthorizationService, ) {} @@ -151,22 +153,25 @@ export class ViewComponent implements OnDestroy, OnInit { this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); this.showPremiumRequiredTotp = - this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; + this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId; this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ this.collectionId as CollectionId, ]); + this.showUpgradeRequiredTotp = + this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp; + if (this.cipher.folderId) { this.folder = await ( await firstValueFrom(this.folderService.folderViews$(activeUserId)) ).find((f) => f.id == this.cipher.folderId); } - if ( - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - (cipher.organizationUseTotp || this.canAccessPremium) - ) { + const canGenerateTotp = this.cipher.organizationId + ? this.cipher.organizationUseTotp + : this.canAccessPremium; + + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) { await this.totpUpdateCode(); const interval = this.totpService.getTimeInterval(this.cipher.login.totp); await this.totpTick(interval); @@ -242,11 +247,13 @@ export class ViewComponent implements OnDestroy, OnInit { try { await this.deleteCipher(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); this.onDeletedCipher.emit(this.cipher); } catch (e) { this.logService.error(e); @@ -262,7 +269,11 @@ export class ViewComponent implements OnDestroy, OnInit { try { await this.restoreCipher(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); this.onRestoredCipher.emit(this.cipher); } catch (e) { this.logService.error(e); @@ -345,13 +356,17 @@ export class ViewComponent implements OnDestroy, OnInit { const matches = await this.checkPasswordPromise; if (matches > 0) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("passwordExposed", matches.toString()), - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("passwordExposed", matches.toString()), + }); } else { - this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordSafe"), + }); } } @@ -381,11 +396,11 @@ export class ViewComponent implements OnDestroy, OnInit { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (typeI18nKey === "password") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -418,11 +433,11 @@ export class ViewComponent implements OnDestroy, OnInit { } if (this.cipher.organizationId == null && !this.canAccessPremium) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("premiumRequired"), - this.i18nService.t("premiumRequiredDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); return; } @@ -446,7 +461,11 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = true; const response = await fetch(new Request(url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -462,8 +481,14 @@ export class ViewComponent implements OnDestroy, OnInit { fileName: attachment.fileName, blobData: decBuf, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts index e278113a653..ba19cf808ee 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts @@ -9,6 +9,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts index 20550e0e8cf..8b406877a12 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -9,6 +9,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index dd0b49f356a..96fb74ba96b 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -4,14 +4,12 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, from, map, mergeMap, Observable, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -22,6 +20,8 @@ import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service"; import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state"; const NestingDelimiter = "/"; @@ -54,9 +54,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async buildOrganizations(): Promise { - let organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + let organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (organizations != null) { - organizations = organizations.filter(isMember).sort((a, b) => a.name.localeCompare(b.name)); + organizations = organizations + .filter((o) => o.isMember) + .sort((a, b) => a.name.localeCompare(b.name)); } return organizations; diff --git a/libs/angular/tsconfig.json b/libs/angular/tsconfig.json index 6004a56fb55..b638410a6a8 100644 --- a/libs/angular/tsconfig.json +++ b/libs/angular/tsconfig.json @@ -1,5 +1,26 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-components": ["../tools/generator/components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/importer/core": ["../importer/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault": ["../vault/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/auth/jest.config.js b/libs/auth/jest.config.js index 8bc834c7dab..121d423be17 100644 --- a/libs/auth/jest.config.js +++ b/libs/auth/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts index b07504b7c8d..9f504c75d29 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts @@ -18,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ButtonModule } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; import { LockIcon } from "../icons"; import { RegistrationCheckEmailIcon } from "../icons/registration-check-email.icon"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index 91229f38ab2..05ddb9614f1 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -9,8 +9,14 @@ import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconModule, Icon } from "../../../../components/src/icon"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../../../components/src/shared"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../components/src/typography"; import { BitwardenLogo, BitwardenShield } from "../icons"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index 27eb27c53b9..c7e15d9dcfa 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -7,7 +7,11 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service"; import { LockIcon } from "../icons"; diff --git a/libs/auth/src/angular/icons/device-verification.icon.ts b/libs/auth/src/angular/icons/device-verification.icon.ts new file mode 100644 index 00000000000..b1be4efdfb3 --- /dev/null +++ b/libs/auth/src/angular/icons/device-verification.icon.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "@bitwarden/components"; + +export const DeviceVerificationIcon = svgIcon` + + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 0e86ee7fc8e..0ec92d54547 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -12,3 +12,4 @@ export * from "./registration-lock-alt.icon"; export * from "./registration-expired-link.icon"; export * from "./sso-key.icon"; export * from "./two-factor-timeout.icon"; +export * from "./device-verification.icon"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 66111f3e5af..67ab68852b2 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -71,3 +71,6 @@ export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.com // login approval export * from "./login-approval/login-approval.component"; export * from "./login-approval/default-login-approval-component.service"; + +// device verification +export * from "./new-device-verification/new-device-verification.component"; diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 94baecb9ef2..c613cf5f533 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -26,7 +26,11 @@ import { } from "@bitwarden/components"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InputsFieldMatch } from "../../../../angular/src/auth/validators/inputs-field-match.validator"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../../../components/src/shared"; import { PasswordCalloutComponent } from "../password-callout/password-callout.component"; diff --git a/libs/auth/src/angular/input-password/input-password.stories.ts b/libs/auth/src/angular/input-password/input-password.stories.ts index 99c0aba81b8..41577328f87 100644 --- a/libs/auth/src/angular/input-password/input-password.stories.ts +++ b/libs/auth/src/angular/input-password/input-password.stories.ts @@ -14,7 +14,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; import { InputPasswordComponent } from "./input-password.component"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.html b/libs/auth/src/angular/login-approval/login-approval.component.html index ddbc48d71a3..c0cb9b9caf4 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.html +++ b/libs/auth/src/angular/login-approval/login-approval.component.html @@ -1,23 +1,31 @@ {{ "areYouTryingtoLogin" | i18n }} -

{{ "logInAttemptBy" | i18n: email }}

-
- {{ "fingerprintPhraseHeader" | i18n }} -

{{ fingerprintPhrase }}

-
-
- {{ "deviceType" | i18n }} -

{{ authRequestResponse?.requestDeviceType }}

-
-
- {{ "ipAddress" | i18n }} -

{{ authRequestResponse?.requestIpAddress }}

-
-
- {{ "time" | i18n }} -

{{ requestTimeText }}

-
+ +
+ +
+
+ + +

{{ "logInAttemptBy" | i18n: email }}

+
+ {{ "fingerprintPhraseHeader" | i18n }} +

{{ fingerprintPhrase }}

+
+
+ {{ "deviceType" | i18n }} +

{{ authRequestResponse?.requestDeviceType }}

+
+
+ {{ "ipAddress" | i18n }} +

{{ authRequestResponse?.requestIpAddress }}

+
+
+ {{ "time" | i18n }} +

{{ requestTimeText }}

+
+
@@ -34,7 +42,7 @@

{{ "logInAttemptBy" | i18n: email }}

type="button" buttonType="secondary" [bitAction]="denyLogin" - [bitDialogClose]="true" + [disabled]="loading" > {{ "denyLogIn" | i18n }} diff --git a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts index ff598bdeb91..da30df62fff 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts @@ -13,6 +13,7 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -29,6 +30,7 @@ describe("LoginApprovalComponent", () => { let i18nService: MockProxy; let dialogRef: MockProxy; let toastService: MockProxy; + let validationService: MockProxy; const testNotificationId = "test-notification-id"; const testEmail = "test@bitwarden.com"; @@ -41,6 +43,7 @@ describe("LoginApprovalComponent", () => { i18nService = mock(); dialogRef = mock(); toastService = mock(); + validationService = mock(); accountService.activeAccount$ = of({ email: testEmail, @@ -62,6 +65,7 @@ describe("LoginApprovalComponent", () => { { provide: KeyService, useValue: mock() }, { provide: DialogRef, useValue: dialogRef }, { provide: ToastService, useValue: toastService }, + { provide: ValidationService, useValue: validationService }, { provide: LoginApprovalComponentServiceAbstraction, useValue: mock(), diff --git a/libs/auth/src/angular/login-approval/login-approval.component.ts b/libs/auth/src/angular/login-approval/login-approval.component.ts index 5192334a0ca..3b44f545abb 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.ts @@ -16,6 +16,7 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AsyncActionsModule, @@ -40,6 +41,8 @@ export interface LoginApprovalDialogParams { imports: [CommonModule, AsyncActionsModule, ButtonModule, DialogModule, JslibModule], }) export class LoginApprovalComponent implements OnInit, OnDestroy { + loading = true; + notificationId: string; private destroy$ = new Subject(); @@ -62,25 +65,25 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { private dialogRef: DialogRef, private toastService: ToastService, private loginApprovalComponentService: LoginApprovalComponentService, + private validationService: ValidationService, ) { this.notificationId = params.notificationId; } async ngOnDestroy(): Promise { clearInterval(this.interval); - const closedWithButton = await firstValueFrom(this.dialogRef.closed); - if (!closedWithButton) { - // 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.retrieveAuthRequestAndRespond(false); - } this.destroy$.next(); this.destroy$.complete(); } async ngOnInit() { if (this.notificationId != null) { - this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); + try { + this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); + } catch (error) { + this.validationService.showError(error); + } + const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey); this.email = await await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), @@ -96,6 +99,8 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { }, RequestTimeUpdate); this.loginApprovalComponentService.showLoginRequestedAlertIfWindowNotVisible(this.email); + + this.loading = false; } } @@ -131,6 +136,8 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { ); this.showResultToast(loginResponse); } + + this.dialogRef.close(approve); } showResultToast(loginResponse: AuthRequestResponse) { diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 5600077c363..a3f5e062e4f 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -107,7 +107,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private validationService: ValidationService, ) { - this.clientType === this.platformUtilsService.getClientType(); + this.clientType = this.platformUtilsService.getClientType(); } async ngOnInit() { diff --git a/libs/auth/src/angular/login/login-secondary-content.component.ts b/libs/auth/src/angular/login/login-secondary-content.component.ts index dbc9535e67a..b608542b375 100644 --- a/libs/auth/src/angular/login/login-secondary-content.component.ts +++ b/libs/auth/src/angular/login/login-secondary-content.component.ts @@ -3,7 +3,6 @@ import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service"; import { LinkModule } from "@bitwarden/components"; @@ -13,16 +12,12 @@ import { LinkModule } from "@bitwarden/components"; template: `
`, }) export class LoginSecondaryContentComponent { - registerRouteService = inject(RegisterRouteService); serverSettingsService = inject(DefaultServerSettingsService); - // TODO: remove when email verification flag is removed - protected registerRoute$ = this.registerRouteService.registerRoute$(); - protected isUserRegistrationDisabled$ = this.serverSettingsService.isUserRegistrationDisabled$; } diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 40f85e6d75c..66fe2503508 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -275,6 +275,12 @@ export class LoginComponent implements OnInit, OnDestroy { return; } + // Redirect to device verification if this is an unknown device + if (authResult.requiresDeviceVerification) { + await this.router.navigate(["device-verification"]); + return; + } + await this.loginSuccessHandlerService.run(authResult.userId); if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { @@ -484,6 +490,8 @@ export class LoginComponent implements OnInit, OnDestroy { try { const deviceIdentifier = await this.appIdService.getAppId(); this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.isKnownDevice = false; } diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.html b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html new file mode 100644 index 00000000000..2f807d32993 --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html @@ -0,0 +1,36 @@ +
+ + {{ "verificationCode" | i18n }} + + + + + +
+ +
+
diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts new file mode 100644 index 00000000000..6e0f9eec05e --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -0,0 +1,163 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; + +import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service"; +import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service"; +import { PasswordLoginStrategy } from "../../common/login-strategies/password-login.strategy"; + +/** + * Component for verifying a new device via a one-time password (OTP). + */ +@Component({ + standalone: true, + selector: "app-new-device-verification", + templateUrl: "./new-device-verification.component.html", + imports: [ + CommonModule, + ReactiveFormsModule, + AsyncActionsModule, + JslibModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ], +}) +export class NewDeviceVerificationComponent implements OnInit, OnDestroy { + formGroup = this.formBuilder.group({ + code: [ + "", + { + validators: [Validators.required], + updateOn: "change", + }, + ], + }); + + protected disableRequestOTP = false; + private destroy$ = new Subject(); + protected authenticationSessionTimeoutRoute = "/authentication-timeout"; + + constructor( + private router: Router, + private formBuilder: FormBuilder, + private passwordLoginStrategy: PasswordLoginStrategy, + private apiService: ApiService, + private loginStrategyService: LoginStrategyServiceAbstraction, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private syncService: SyncService, + private loginEmailService: LoginEmailServiceAbstraction, + ) {} + + async ngOnInit() { + // Redirect to timeout route if session expires + this.loginStrategyService.authenticationSessionTimeout$ + .pipe(takeUntil(this.destroy$)) + .subscribe((expired) => { + if (!expired) { + return; + } + + try { + void this.router.navigate([this.authenticationSessionTimeoutRoute]); + } catch (err) { + this.logService.error( + `Failed to navigate to ${this.authenticationSessionTimeoutRoute} route`, + err, + ); + } + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Resends the OTP for device verification. + */ + async resendOTP() { + this.disableRequestOTP = true; + try { + const email = await this.loginStrategyService.getEmail(); + const masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash(); + + if (!email || !masterPasswordHash) { + throw new Error("Missing email or master password hash"); + } + + await this.apiService.send( + "POST", + "/accounts/resend-new-device-otp", + { + email: email, + masterPasswordHash: masterPasswordHash, + }, + false, + false, + ); + } catch (e) { + this.logService.error(e); + } finally { + this.disableRequestOTP = false; + } + } + + /** + * Submits the OTP for device verification. + */ + submit = async (): Promise => { + const codeControl = this.formGroup.get("code"); + if (!codeControl || !codeControl.value) { + return; + } + + try { + const authResult = await this.loginStrategyService.logInNewDeviceVerification( + codeControl.value, + ); + + if (authResult.requiresTwoFactor) { + await this.router.navigate(["/2fa"]); + return; + } + + if (authResult.forcePasswordReset) { + await this.router.navigate(["/update-temp-password"]); + return; + } + + this.loginEmailService.clearValues(); + + await this.syncService.fullSync(true); + + // If verification succeeds, navigate to vault + await this.router.navigate(["/vault"]); + } catch (e) { + this.logService.error(e); + const errorMessage = + (e as any)?.response?.error_description ?? this.i18nService.t("errorOccurred"); + codeControl.setErrors({ serverError: { message: errorMessage } }); + } + }; +} diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 31b3f7db92a..c419e1f427f 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -16,7 +16,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; -import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common"; +import { + LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, + PasswordLoginCredentials, +} from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent } from "../../input-password/input-password.component"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -68,6 +72,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) {} async ngOnInit() { @@ -189,6 +194,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { message: this.i18nService.t("youHaveBeenLoggedIn"), }); + await this.loginSuccessHandlerService.run(authenticationResult.userId); + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index fa3ad2ae2b9..6047cc3d27a 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -28,7 +28,8 @@ import { } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests"; import { LoginEmailService } from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts index 0557227938f..b54529f6a2c 100644 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts +++ b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts @@ -15,6 +15,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ToastService } from "../../../../components/src/toast"; import { InputPasswordComponent } from "../input-password/input-password.component"; import { PasswordInputResult } from "../input-password/password-input-result"; diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index aad0df4e397..4583332cb88 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -12,6 +12,7 @@ import { TrustedDeviceUserDecryptionOption, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, + LoginSuccessHandlerService, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; @@ -35,7 +36,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { AsyncActionsModule, ButtonModule, @@ -117,7 +117,7 @@ export class SsoComponent implements OnInit { private accountService: AccountService, private toastService: ToastService, private ssoComponentService: SsoComponentService, - private syncService: SyncService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) { environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; @@ -378,8 +378,7 @@ export class SsoComponent implements OnInit { // Everything after the 2FA check is considered a successful login // Just have to figure out where to send the user - - await this.syncService.fullSync(true); + await this.loginSuccessHandlerService.run(authResult.userId); // Save off the OrgSsoIdentifier for use in the TDE flows (or elsewhere) // - TDE login decryption options component @@ -481,16 +480,7 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - let route = "set-password"; - if (emailVerification) { - route = "set-password-jit"; - } - - await this.router.navigate([route], { + await this.router.navigate(["set-password-jit"], { queryParams: { identifier: orgIdentifier, }, diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 1088d6de736..bd725f29024 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -47,7 +47,6 @@ export abstract class LoginStrategyServiceAbstraction { * Auth Request. Otherwise, it will return null. */ getAuthRequestId: () => Promise; - /** * Sends a token request to the server using the provided credentials. */ @@ -74,7 +73,11 @@ export abstract class LoginStrategyServiceAbstraction { */ makePreloginKey: (masterPassword: string, email: string) => Promise; /** - * Emits true if the two factor session has expired. + * Emits true if the authentication session has expired. + */ + authenticationSessionTimeout$: Observable; + /** + * Sends a token request to the server with the provided device verification OTP. */ - twoFactorTimeout$: Observable; + logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise; } diff --git a/libs/auth/src/common/index.ts b/libs/auth/src/common/index.ts index 43efd7c6387..97909bdc449 100644 --- a/libs/auth/src/common/index.ts +++ b/libs/auth/src/common/index.ts @@ -6,3 +6,4 @@ export * from "./models"; export * from "./types"; export * from "./services"; export * from "./utilities"; +export * from "./login-strategies"; diff --git a/libs/auth/src/common/login-strategies/index.ts b/libs/auth/src/common/login-strategies/index.ts new file mode 100644 index 00000000000..166ef935e08 --- /dev/null +++ b/libs/auth/src/common/login-strategies/index.ts @@ -0,0 +1 @@ +export { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 50443bab0ea..a8208a1e0ad 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -12,6 +13,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -76,8 +78,8 @@ const twoFactorToken = "TWO_FACTOR_TOKEN"; const twoFactorRemember = true; export function identityTokenResponseFactory( - masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null, - userDecryptionOptions: IUserDecryptionOptionsServerResponse = null, + masterPasswordPolicyResponse: MasterPasswordPolicyResponse | undefined = undefined, + userDecryptionOptions: IUserDecryptionOptionsServerResponse | undefined = undefined, ) { return new IdentityTokenResponse({ ForcePasswordReset: false, @@ -155,7 +157,7 @@ describe("LoginStrategy", () => { passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as unknown as AccountService, masterPasswordService, keyService, encryptService, @@ -286,13 +288,16 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); - expect(result).toEqual({ - userId: userId, - forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset, - resetMasterPassword: true, - twoFactorProviders: null, - captchaSiteKey: "", - } as AuthResult); + const expected = new AuthResult(); + expected.userId = userId; + expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; + expected.resetMasterPassword = true; + expected.twoFactorProviders = {} as Partial< + Record> + >; + expected.captchaSiteKey = ""; + expected.twoFactorProviders = null; + expect(result).toEqual(expected); }); it("rejects login if CAPTCHA is required", async () => { @@ -377,10 +382,11 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = { 0: null } as Record< - TwoFactorProviderType, - Record + expected.twoFactorProviders = { 0: null } as unknown as Partial< + Record> >; + expected.email = ""; + expected.ssoEmail2FaSessionToken = undefined; expect(result).toEqual(expected); }); @@ -460,14 +466,19 @@ describe("LoginStrategy", () => { it("sends 2FA token provided by user to server (two-step)", async () => { // Simulate a partially completed login cache = new PasswordLoginStrategyData(); - cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); passwordLoginStrategy = new PasswordLoginStrategy( cache, passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as AccountService, masterPasswordService, keyService, encryptService, @@ -489,7 +500,7 @@ describe("LoginStrategy", () => { await passwordLoginStrategy.logInTwoFactor( new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember), - null, + "", ); expect(apiService.postIdentityToken).toHaveBeenCalledWith( @@ -503,4 +514,54 @@ describe("LoginStrategy", () => { ); }); }); + + describe("Device verification", () => { + it("processes device verification response", async () => { + const captchaToken = "test-captcha-token"; + const deviceVerificationResponse = new IdentityDeviceVerificationResponse({ + error: "invalid_grant", + error_description: "Device verification required.", + email: "test@bitwarden.com", + deviceVerificationRequest: true, + captchaToken: captchaToken, + }); + + apiService.postIdentityToken.mockResolvedValue(deviceVerificationResponse); + + cache = new PasswordLoginStrategyData(); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); + + passwordLoginStrategy = new PasswordLoginStrategy( + cache, + passwordStrengthService, + policyService, + loginStrategyService, + accountService as AccountService, + masterPasswordService, + keyService, + encryptService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + twoFactorService, + userDecryptionOptionsService, + billingAccountProfileStateService, + vaultTimeoutSettingsService, + kdfConfigService, + ); + + const result = await passwordLoginStrategy.logIn(credentials); + + expect(result.requiresDeviceVerification).toBe(true); + }); + }); }); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 25f99f47840..6b1dcfb155c 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,6 +16,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -51,14 +50,19 @@ import { import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; +type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse; export abstract class LoginStrategyData { tokenRequest: | UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest - | WebAuthnLoginTokenRequest; + | WebAuthnLoginTokenRequest + | undefined; captchaBypassToken?: string; /** User's entered email obtained pre-login. */ @@ -67,6 +71,8 @@ export abstract class LoginStrategyData { export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; + protected sessionTimeoutSubject = new BehaviorSubject(false); + sessionTimeout$: Observable = this.sessionTimeoutSubject.asObservable(); constructor( protected accountService: AccountService, @@ -100,9 +106,12 @@ export abstract class LoginStrategy { async logInTwoFactor( twoFactor: TokenTwoFactorRequest, - captchaResponse: string = null, + captchaResponse: string | null = null, ): Promise { const data = this.cache.value; + if (!data.tokenRequest) { + throw new Error("Token request is undefined"); + } data.tokenRequest.setTwoFactor(twoFactor); this.cache.next(data); const [authResult] = await this.startLogIn(); @@ -113,6 +122,9 @@ export abstract class LoginStrategy { await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; + if (!tokenRequest) { + throw new Error("Token request is undefined"); + } const response = await this.apiService.postIdentityToken(tokenRequest); if (response instanceof IdentityTwoFactorResponse) { @@ -121,6 +133,8 @@ export abstract class LoginStrategy { return [await this.processCaptchaResponse(response), response]; } else if (response instanceof IdentityTokenResponse) { return [await this.processTokenResponse(response), response]; + } else if (response instanceof IdentityDeviceVerificationResponse) { + return [await this.processDeviceVerificationResponse(response), response]; } throw new Error("Invalid response object."); @@ -176,8 +190,8 @@ export abstract class LoginStrategy { await this.accountService.addAccount(userId, { name: accountInformation.name, - email: accountInformation.email, - emailVerified: accountInformation.email_verified, + email: accountInformation.email ?? "", + emailVerified: accountInformation.email_verified ?? false, }); await this.accountService.switchAccount(userId); @@ -230,7 +244,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium( - accountInformation.premium, + accountInformation.premium ?? false, false, userId, ); @@ -291,6 +305,9 @@ export abstract class LoginStrategy { try { const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); + if (!privateKey.encryptedString) { + throw new Error("Failed to create encrypted private key"); + } await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { @@ -316,7 +333,8 @@ export abstract class LoginStrategy { await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; - result.email = response.email; + + result.email = response.email ?? ""; return result; } @@ -355,4 +373,22 @@ export abstract class LoginStrategy { ), ); } + + /** + * Handles the response from the server when a device verification is required. + * It sets the requiresDeviceVerification flag to true and caches the captcha token if it came back. + * + * @param {IdentityDeviceVerificationResponse} response - The response from the server indicating that device verification is required. + * @returns {Promise} - A promise that resolves to an AuthResult object + */ + protected async processDeviceVerificationResponse( + response: IdentityDeviceVerificationResponse, + ): Promise { + const result = new AuthResult(); + result.requiresDeviceVerification = true; + + // Extend cached data with captcha bypass token if it came back. + this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); + return result; + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 4ee4fcaeb38..d572710a2fd 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -276,4 +276,24 @@ describe("PasswordLoginStrategy", () => { ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); + + it("handles new device verification login with OTP", async () => { + const deviceVerificationOtp = "123456"; + const tokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValueOnce(tokenResponse); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); + + await passwordLoginStrategy.logIn(credentials); + + const result = await passwordLoginStrategy.logInNewDeviceVerification(deviceVerificationOtp); + + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None); + expect(result.resetMasterPassword).toBe(false); + expect(result.userId).toBe(userId); + }); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index c496b7c9674..f0a8d40f914 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -10,6 +10,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { HashPurpose } from "@bitwarden/common/platform/enums"; @@ -208,9 +209,12 @@ export class PasswordLoginStrategy extends LoginStrategy { } private getMasterPasswordPolicyOptionsFromResponse( - response: IdentityTokenResponse | IdentityTwoFactorResponse, + response: + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse, ): MasterPasswordPolicyOptions { - if (response == null) { + if (response == null || response instanceof IdentityDeviceVerificationResponse) { return null; } return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); @@ -233,4 +237,13 @@ export class PasswordLoginStrategy extends LoginStrategy { password: this.cache.value, }; } + + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + const data = this.cache.value; + data.tokenRequest.newDeviceOtp = deviceVerificationOtp; + this.cache.next(data); + + const [authResult] = await this.startLogIn(); + return authResult; + } } diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts index 95efe2b0077..00b78064d83 100644 --- a/libs/auth/src/common/models/domain/user-decryption-options.ts +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -4,6 +4,8 @@ import { Jsonify } from "type-fest"; import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response"; import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response"; /** diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index d1cedebcf36..44f6afa5d23 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -4,6 +4,5 @@ export * from "./login-strategies/login-strategy.service"; export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; export * from "./auth-request/auth-request-api.service"; -export * from "./register-route.service"; export * from "./accounts/lock.service"; export * from "./login-success-handler/default-login-success-handler.service"; diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts index feb0cba1bdc..aa13afd5004 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -6,6 +6,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { GlobalState, KeyDefinition, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 5fcbefbef2f..3b03e8754bc 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -321,4 +321,67 @@ describe("LoginStrategyService", () => { `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1}; possible pre-login downgrade attack detected.`, ); }); + + it("returns an AuthResult on successful new device verification", async () => { + const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD"); + const deviceVerificationOtp = "123456"; + + // Setup initial login and device verification response + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTwoFactorResponse({ + TwoFactorProviders: ["0"], + TwoFactorProviders2: { 0: null }, + error: "invalid_grant", + error_description: "Two factor required.", + email: undefined, + ssoEmail2faSessionToken: undefined, + }), + ); + + await sut.logIn(credentials); + + // Successful device verification login + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTokenResponse({ + ForcePasswordReset: false, + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + Key: "KEY", + PrivateKey: "PRIVATE_KEY", + ResetMasterPassword: false, + access_token: "ACCESS_TOKEN", + expires_in: 3600, + refresh_token: "REFRESH_TOKEN", + scope: "api offline_access", + token_type: "Bearer", + }), + ); + + tokenService.decodeAccessToken.calledWith("ACCESS_TOKEN").mockResolvedValue({ + sub: "USER_ID", + name: "NAME", + email: "EMAIL", + premium: false, + }); + + const result = await sut.logInNewDeviceVerification(deviceVerificationOtp); + + expect(result).toBeInstanceOf(AuthResult); + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + }); }); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 1f98a117c88..e3a20fcfe72 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { combineLatestWith, distinctUntilChanged, @@ -15,6 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -35,7 +34,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; import { @@ -49,12 +47,24 @@ import { import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; -import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; +import { + AuthRequestLoginStrategy, + AuthRequestLoginStrategyData, +} from "../../login-strategies/auth-request-login.strategy"; import { LoginStrategy } from "../../login-strategies/login.strategy"; -import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; -import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; -import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy"; -import { WebAuthnLoginStrategy } from "../../login-strategies/webauthn-login.strategy"; +import { + PasswordLoginStrategy, + PasswordLoginStrategyData, +} from "../../login-strategies/password-login.strategy"; +import { SsoLoginStrategy, SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy"; +import { + UserApiLoginStrategy, + UserApiLoginStrategyData, +} from "../../login-strategies/user-api-login.strategy"; +import { + WebAuthnLoginStrategy, + WebAuthnLoginStrategyData, +} from "../../login-strategies/webauthn-login.strategy"; import { UserApiLoginCredentials, PasswordLoginCredentials, @@ -74,14 +84,15 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { - private sessionTimeoutSubscription: Subscription; + private sessionTimeoutSubscription: Subscription | undefined; private currentAuthnTypeState: GlobalState; private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; - private authRequestPushNotificationState: GlobalState; - private twoFactorTimeoutSubject = new BehaviorSubject(false); + private authRequestPushNotificationState: GlobalState; + private authenticationTimeoutSubject = new BehaviorSubject(false); - twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); + authenticationSessionTimeout$: Observable = + this.authenticationTimeoutSubject.asObservable(); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -130,7 +141,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { - this.twoFactorTimeoutSubject.next(true); + this.authenticationTimeoutSubject.next(true); try { await this.clearCache(); } catch (e) { @@ -151,7 +162,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getEmail(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("email$" in strategy) { + if (strategy && "email$" in strategy) { return await firstValueFrom(strategy.email$); } return null; @@ -160,7 +171,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getMasterPasswordHash(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("serverMasterKeyHash$" in strategy) { + if (strategy && "serverMasterKeyHash$" in strategy) { return await firstValueFrom(strategy.serverMasterKeyHash$); } return null; @@ -169,7 +180,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getSsoEmail2FaSessionToken(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("ssoEmail2FaSessionToken$" in strategy) { + if (strategy && "ssoEmail2FaSessionToken$" in strategy) { return await firstValueFrom(strategy.ssoEmail2FaSessionToken$); } return null; @@ -178,7 +189,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAccessCode(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("accessCode$" in strategy) { + if (strategy && "accessCode$" in strategy) { return await firstValueFrom(strategy.accessCode$); } return null; @@ -187,7 +198,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAuthRequestId(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("authRequestId$" in strategy) { + if (strategy && "authRequestId$" in strategy) { return await firstValueFrom(strategy.authRequestId$); } return null; @@ -202,7 +213,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { | WebAuthnLoginCredentials, ): Promise { await this.clearCache(); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.currentAuthnTypeState.update((_) => credentials.type); @@ -215,16 +226,19 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // If the popup uses its own instance of this service, this can be removed. const ownedCredentials = { ...credentials }; - const result = await strategy.logIn(ownedCredentials as any); + const result = await strategy?.logIn(ownedCredentials as any); - if (result != null && !result.requiresTwoFactor) { + if (result != null && !result.requiresTwoFactor && !result.requiresDeviceVerification) { await this.clearCache(); } else { - // Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts - await this.loginStrategyCacheState.update((_) => strategy.exportCache()); + // Cache the strategy data so we can attempt again later with 2fa or device verification + await this.loginStrategyCacheState.update((_) => strategy?.exportCache() ?? null); await this.startSessionTimeout(); } + if (!result) { + throw new Error("No auth result returned"); + } return result; } @@ -258,9 +272,46 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { } } + /** + * Sends a token request to the server with the provided device verification OTP. + * Returns an error if no session data is found or if the current login strategy does not support device verification. + * @param deviceVerificationOtp The OTP to send to the server for device verification. + * @returns The result of the token request. + */ + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + if (!(await this.isSessionValid())) { + throw new Error(this.i18nService.t("sessionTimeout")); + } + + const strategy = await firstValueFrom(this.loginStrategy$); + if (strategy == null) { + throw new Error("No login strategy found."); + } + + if (!("logInNewDeviceVerification" in strategy)) { + throw new Error("Current login strategy does not support device verification."); + } + + try { + const result = await strategy.logInNewDeviceVerification(deviceVerificationOtp); + + // Only clear cache if device verification succeeds + if (result !== null && !result.requiresDeviceVerification) { + await this.clearCache(); + } + return result; + } catch (e) { + // Clear the cache if there is an unhandled client-side error + if (!(e instanceof ErrorResponse)) { + await this.clearCache(); + } + throw e; + } + } + async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdfConfig: KdfConfig = null; + let kdfConfig: KdfConfig | undefined; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { @@ -273,12 +324,15 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { preloginResponse.kdfParallelism, ); } - } catch (e) { + } catch (e: any) { if (e == null || e.statusCode !== 404) { throw e; } } + if (!kdfConfig) { + throw new Error("KDF config is required"); + } kdfConfig.validateKdfConfigForPrelogin(); return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig); @@ -287,7 +341,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async clearCache(): Promise { await this.currentAuthnTypeState.update((_) => null); await this.loginStrategyCacheState.update((_) => null); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.clearSessionTimeout(); } @@ -358,7 +412,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { switch (strategy) { case AuthenticationType.Password: return new PasswordLoginStrategy( - data?.password, + data?.password ?? new PasswordLoginStrategyData(), this.passwordStrengthService, this.policyService, this, @@ -366,7 +420,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.Sso: return new SsoLoginStrategy( - data?.sso, + data?.sso ?? new SsoLoginStrategyData(), this.keyConnectorService, this.deviceTrustService, this.authRequestService, @@ -375,19 +429,22 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( - data?.userApiKey, + data?.userApiKey ?? new UserApiLoginStrategyData(), this.environmentService, this.keyConnectorService, ...sharedDeps, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( - data?.authRequest, + data?.authRequest ?? new AuthRequestLoginStrategyData(), this.deviceTrustService, ...sharedDeps, ); case AuthenticationType.WebAuthn: - return new WebAuthnLoginStrategy(data?.webAuthn, ...sharedDeps); + return new WebAuthnLoginStrategy( + data?.webAuthn ?? new WebAuthnLoginStrategyData(), + ...sharedDeps, + ); } }), ); diff --git a/libs/auth/src/common/services/register-route.service.ts b/libs/auth/src/common/services/register-route.service.ts deleted file mode 100644 index 5bc09db699e..00000000000 --- a/libs/auth/src/common/services/register-route.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Observable, map } from "rxjs"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -// This is a temporary service to determine the correct route to use for registration based on the email verification feature flag. -export class RegisterRouteService { - constructor(private configService: ConfigService) {} - - registerRoute$(): Observable { - return this.configService.getFeatureFlag$(FeatureFlag.EmailVerification).pipe( - map((emailVerificationEnabled) => { - if (emailVerificationEnabled) { - return "/signup"; - } else { - return "/register"; - } - }), - ); - } -} diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts index ffb660e6a7f..7c44a6f1682 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -8,6 +8,8 @@ import { USER_DECRYPTION_OPTIONS_DISK, UserKeyDefinition, } from "@bitwarden/common/platform/state"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserId } from "@bitwarden/common/src/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; diff --git a/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts b/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts index 717e80b110d..24b3adacc21 100644 --- a/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts +++ b/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts @@ -18,6 +18,8 @@ export function decodeJwtTokenToJson(jwtToken: string): any { try { // Attempt to decode from URL-safe Base64 to UTF-8 decodedPayloadJSON = Utils.fromUrlB64ToUtf8(encodedPayload); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (decodingError) { throw new Error("Cannot decode the token"); } @@ -26,6 +28,8 @@ export function decodeJwtTokenToJson(jwtToken: string): any { // Attempt to parse the JSON payload const decodedToken = JSON.parse(decodedPayloadJSON); return decodedToken; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (jsonError) { throw new Error("Cannot parse the token's payload into JSON"); } diff --git a/libs/auth/tsconfig.json b/libs/auth/tsconfig.json index 6004a56fb55..8d08522ffce 100644 --- a/libs/auth/tsconfig.json +++ b/libs/auth/tsconfig.json @@ -1,5 +1,23 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "resolveJsonModule": true, + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/billing/jest.config.js b/libs/billing/jest.config.js index d9bae9633ea..c43606191b9 100644 --- a/libs/billing/jest.config.js +++ b/libs/billing/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/billing/tsconfig.json b/libs/billing/tsconfig.json index 6004a56fb55..bb08eb89d1c 100644 --- a/libs/billing/tsconfig.json +++ b/libs/billing/tsconfig.json @@ -1,5 +1,6 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": {}, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/common/jest.config.js b/libs/common/jest.config.js index d7f78abbf38..7e6c0997b9c 100644 --- a/libs/common/jest.config.js +++ b/libs/common/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../shared/jest.config.ts"); diff --git a/libs/common/spec/matchers/index.ts b/libs/common/spec/matchers/index.ts index 44440be5b54..b2e09cc8e92 100644 --- a/libs/common/spec/matchers/index.ts +++ b/libs/common/spec/matchers/index.ts @@ -1,16 +1,12 @@ -import * as matchers from "jest-extended"; - import { toBeFulfilled, toBeResolved, toBeRejected } from "./promise-fulfilled"; import { toAlmostEqual } from "./to-almost-equal"; +import { toContainPartialObjects } from "./to-contain-partial-objects"; import { toEqualBuffer } from "./to-equal-buffer"; export * from "./to-equal-buffer"; export * from "./to-almost-equal"; export * from "./promise-fulfilled"; -// add all jest-extended matchers -expect.extend(matchers); - export function addCustomMatchers() { expect.extend({ toEqualBuffer: toEqualBuffer, @@ -18,6 +14,7 @@ export function addCustomMatchers() { toBeFulfilled: toBeFulfilled, toBeResolved: toBeResolved, toBeRejected: toBeRejected, + toContainPartialObjects, }); } @@ -59,4 +56,9 @@ export interface CustomMatchers { * @returns CustomMatcherResult indicating whether or not the test passed */ toBeRejected(withinMs?: number): Promise; + /** + * Matches if the received array contains all the expected objects using partial matching (expect.objectContaining). + * @param expected An array of partial objects that should be contained in the received array. + */ + toContainPartialObjects(expected: Array): R; } diff --git a/libs/common/spec/matchers/to-contain-partial-objects.spec.ts b/libs/common/spec/matchers/to-contain-partial-objects.spec.ts new file mode 100644 index 00000000000..ab6f90adf17 --- /dev/null +++ b/libs/common/spec/matchers/to-contain-partial-objects.spec.ts @@ -0,0 +1,77 @@ +describe("toContainPartialObjects", () => { + describe("matches", () => { + it("if the array only contains the partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 1 }, { id: 2 }]; + + expect(actual).toContainPartialObjects(expected); + }); + + it("if the array contains the partial objects and other objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + { + id: 3, + name: "baz", + }, + ]; + + const expected = [{ id: 1 }, { id: 2 }]; + + expect(actual).toContainPartialObjects(expected); + }); + }); + + describe("doesn't match", () => { + it("if the array does not contain any partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 1, name: "Foo" }]; + + expect(actual).not.toContainPartialObjects(expected); + }); + + it("if the array contains some but not all partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 2 }, { id: 3 }]; + + expect(actual).not.toContainPartialObjects(expected); + }); + }); +}); diff --git a/libs/common/spec/matchers/to-contain-partial-objects.ts b/libs/common/spec/matchers/to-contain-partial-objects.ts new file mode 100644 index 00000000000..f072ca6fba6 --- /dev/null +++ b/libs/common/spec/matchers/to-contain-partial-objects.ts @@ -0,0 +1,31 @@ +import { EOL } from "os"; + +import { diff } from "jest-diff"; + +export const toContainPartialObjects: jest.CustomMatcher = function ( + received: Array, + expected: Array, +) { + const matched = this.equals( + received, + expect.arrayContaining(expected.map((e) => expect.objectContaining(e))), + ); + + if (matched) { + return { + message: () => + "Expected the received array NOT to include partial matches for all expected objects." + + EOL + + diff(expected, received), + pass: true, + }; + } + + return { + message: () => + "Expected the received array to contain partial matches for all expected objects." + + EOL + + diff(expected, received), + pass: false, + }; +}; diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 997974e0581..5bd2221860b 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, + CollectionRequest, CollectionResponse, } from "@bitwarden/admin-console/common"; @@ -70,6 +70,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -95,7 +96,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { TaxRateResponse } from "../billing/models/response/tax-rate.response"; import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; import { EventRequest } from "../models/request/event.request"; import { KdfRequest } from "../models/request/kdf.request"; @@ -137,7 +137,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher */ export abstract class ApiService { send: ( - method: "GET" | "POST" | "PUT" | "DELETE", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, authed: boolean, @@ -152,7 +152,12 @@ export abstract class ApiService { | SsoTokenRequest | UserApiTokenRequest | WebAuthnLoginTokenRequest, - ) => Promise; + ) => Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + >; refreshIdentityToken: () => Promise; getProfile: () => Promise; @@ -376,7 +381,6 @@ export abstract class ApiService { ): Promise>; deleteOrganizationConnection: (id: string) => Promise; getPlans: () => Promise>; - getTaxRates: () => Promise>; getProviderUsers: (providerId: string) => Promise>; getProviderUser: (providerId: string, id: string) => Promise; diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts index 5ecfca5ab84..5a393ed1996 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts @@ -1,7 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ListResponse } from "@bitwarden/common/models/response/list.response"; - +import { ListResponse } from "../../../models/response/list.response"; import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request"; import { OrganizationDomainSsoDetailsResponse } from "./responses/organization-domain-sso-details.response"; diff --git a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts index c4817306a63..066d2d381e7 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../../models/response/base.response"; export class VerifiedOrganizationDomainSsoDetailsResponse extends BaseResponse { organizationName: string; diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 0c45c919e95..000d1655416 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; - import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request"; import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; @@ -13,6 +11,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; +import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index 2161feb516e..05c214ece13 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -17,7 +17,7 @@ export function canAccessSettingsTab(org: Organization): boolean { org.canManageSso || org.canManageScim || org.canAccessImport || - org.canAccessExport(false) || // Feature flag value doesn't matter here, providers will have access to this group anyway + org.canAccessExport || org.canManageDeviceApprovals ); } @@ -57,14 +57,6 @@ export function getOrganizationById(id: string) { return map((orgs) => orgs.find((o) => o.id === id)); } -/** - * Returns `true` if a user is a member of an organization (rather than only being a ProviderUser) - * @deprecated Use organizationService.organizations$ with a filter instead - */ -export function isMember(org: Organization): boolean { - return org.isMember; -} - /** * Publishes an observable stream of organizations. This service is meant to * be used widely across Bitwarden as the primary way of fetching organizations. @@ -73,41 +65,23 @@ export function isMember(org: Organization): boolean { */ export abstract class OrganizationService { /** - * Publishes state for all organizations under the active user. + * Publishes state for all organizations under the specified user. * @returns An observable list of organizations */ - organizations$: Observable; + organizations$: (userId: UserId) => Observable; // @todo Clean these up. Continuing to expand them is not recommended. // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: Observable; - /** - * @deprecated This is currently only used in the CLI, and should not be - * used in any new calls. Use get$ instead for the time being, and we'll be - * removing this method soon. See Jira for details: - * https://bitwarden.atlassian.net/browse/AC-2252. - */ - getFromState: (id: string) => Promise; + memberOrganizations$: (userId: UserId) => Observable; /** * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. */ - canManageSponsorships$: Observable; + canManageSponsorships$: (userId: UserId) => Observable; /** * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. */ - familySponsorshipAvailable$: Observable; - hasOrganizations: () => Promise; - get$: (id: string) => Observable; - get: (id: string) => Promise; - /** - * @deprecated This method is only used in key connector and will be removed soon as part of https://bitwarden.atlassian.net/browse/AC-2252. - */ - getAll: (userId?: string) => Promise; - - /** - * Publishes state for all organizations for the given user id or the active user. - */ - getAll$: (userId?: UserId) => Observable; + familySponsorshipAvailable$: (userId: UserId) => Observable; + hasOrganizations: (userId: UserId) => Observable; } /** @@ -120,20 +94,18 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio /** * Replaces state for the provided organization, or creates it if not found. * @param organization The organization state being saved. - * @param userId The userId to replace state for. Defaults to the active - * user. + * @param userId The userId to replace state for. */ - upsert: (OrganizationData: OrganizationData) => Promise; + upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; /** - * Replaces state for the entire registered organization list for the active user. + * Replaces state for the entire registered organization list for the specified user. * You probably don't want this unless you're calling from a full sync * operation or a logout. See `upsert` for creating & updating a single * organization in the state. - * @param organizations A complete list of all organization state for the active - * user. - * @param userId The userId to replace state for. Defaults to the active + * @param organizations A complete list of all organization state for the provided * user. + * @param userId The userId to replace state for. */ - replace: (organizations: { [id: string]: OrganizationData }, userId?: UserId) => Promise; + replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts b/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts deleted file mode 100644 index b5c0f6291fc..00000000000 --- a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -export function canAccessVaultTab(org: Organization): boolean { - return org.canViewAllCollections; -} - -export function canAccessSettingsTab(org: Organization): boolean { - return ( - org.isOwner || - org.canManagePolicies || - org.canManageSso || - org.canManageScim || - org.canAccessImport || - org.canAccessExport(false) || // Feature flag value doesn't matter here, providers will have access to this group anyway - org.canManageDeviceApprovals - ); -} - -export function canAccessMembersTab(org: Organization): boolean { - return org.canManageUsers || org.canManageUsersPassword; -} - -export function canAccessGroupsTab(org: Organization): boolean { - return org.canManageGroups; -} - -export function canAccessReportingTab(org: Organization): boolean { - return org.canAccessReports || org.canAccessEventLogs; -} - -export function canAccessBillingTab(org: Organization): boolean { - return org.isOwner; -} - -export function canAccessOrgAdmin(org: Organization): boolean { - // Admin console can only be accessed by Owners for disabled organizations - if (!org.enabled && !org.isOwner) { - return false; - } - return ( - canAccessMembersTab(org) || - canAccessGroupsTab(org) || - canAccessReportingTab(org) || - canAccessBillingTab(org) || - canAccessSettingsTab(org) || - canAccessVaultTab(org) - ); -} - -export function getOrganizationById(id: string) { - return map((orgs) => orgs.find((o) => o.id === id)); -} - -/** - * Publishes an observable stream of organizations. This service is meant to - * be used widely across Bitwarden as the primary way of fetching organizations. - * Risky operations like updates are isolated to the - * internal extension `InternalOrganizationServiceAbstraction`. - */ -export abstract class vNextOrganizationService { - /** - * Publishes state for all organizations under the specified user. - * @returns An observable list of organizations - */ - organizations$: (userId: UserId) => Observable; - - // @todo Clean these up. Continuing to expand them is not recommended. - // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: (userId: UserId) => Observable; - /** - * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. - */ - canManageSponsorships$: (userId: UserId) => Observable; - /** - * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. - */ - familySponsorshipAvailable$: (userId: UserId) => Observable; - hasOrganizations: (userId: UserId) => Observable; -} - -/** - * Big scary buttons that **update** organization state. These should only be - * called from within admin-console scoped code. Extends the base - * `OrganizationService` for easy access to `get` calls. - * @internal - */ -export abstract class vNextInternalOrganizationServiceAbstraction extends vNextOrganizationService { - /** - * Replaces state for the provided organization, or creates it if not found. - * @param organization The organization state being saved. - * @param userId The userId to replace state for. - */ - upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; - - /** - * Replaces state for the entire registered organization list for the specified user. - * You probably don't want this unless you're calling from a full sync - * operation or a logout. See `upsert` for creating & updating a single - * organization in the state. - * @param organizations A complete list of all organization state for the provided - * user. - * @param userId The userId to replace state for. - */ - replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; -} diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index bed341115ee..4280756326c 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -30,7 +30,7 @@ export abstract class PolicyService { * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * @param policyType the {@link PolicyType} to search for */ - getAll$: (policyType: PolicyType, userId?: UserId) => Observable; + getAll$: (policyType: PolicyType, userId: UserId) => Observable; /** * All {@link Policy} objects for the specified user (from sync data). diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index da9a82e7c5c..8a49109f343 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -1,6 +1,6 @@ import { ProductTierType } from "../../../billing/enums/product-tier-type.enum"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; -import { ORGANIZATIONS } from "../../services/organization/organization.service"; +import { ORGANIZATIONS } from "../../services/organization/organization.state"; import { OrganizationData } from "./organization.data"; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 8441298bbff..9dcc9f0752c 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -182,11 +182,7 @@ export class Organization { ); } - canAccessExport(removeProviderExport: boolean) { - if (!removeProviderExport && this.isProviderUser) { - return true; - } - + get canAccessExport() { return ( this.isMember && (this.type === OrganizationUserType.Owner || diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts index 7497a77e6f2..1052fe504d4 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts @@ -1,14 +1,13 @@ import { mock } from "jest-mock-extended"; import { lastValueFrom } from "rxjs"; -import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; - import { ApiService } from "../../../abstractions/api.service"; +import { ListResponse } from "../../../models/response/list.response"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; import { OrgDomainApiService } from "./org-domain-api.service"; import { OrgDomainService } from "./org-domain.service"; diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts similarity index 96% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.spec.ts index 9e2ea3a4599..41c89c0e41a 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts @@ -6,11 +6,11 @@ import { OrganizationId, UserId } from "../../../types/guid"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { DefaultvNextOrganizationService } from "./default-vnext-organization.service"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { DefaultOrganizationService } from "./default-organization.service"; +import { ORGANIZATIONS } from "./organization.state"; describe("OrganizationService", () => { - let organizationService: DefaultvNextOrganizationService; + let organizationService: DefaultOrganizationService; const fakeUserId = Utils.newGuid() as UserId; let fakeStateProvider: FakeStateProvider; @@ -86,7 +86,7 @@ describe("OrganizationService", () => { beforeEach(async () => { fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(fakeUserId)); - organizationService = new DefaultvNextOrganizationService(fakeStateProvider); + organizationService = new DefaultOrganizationService(fakeStateProvider); }); describe("canManageSponsorships", () => { diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts b/libs/common/src/admin-console/services/organization/default-organization.service.ts similarity index 92% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.ts index 8b73c271daf..e78136455fd 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.ts @@ -4,11 +4,11 @@ import { map, Observable } from "rxjs"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; -import { vNextInternalOrganizationServiceAbstraction } from "../../abstractions/organization/vnext.organization.service"; +import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { ORGANIZATIONS } from "./organization.state"; /** * Filter out organizations from an observable that __do not__ offer a @@ -41,9 +41,7 @@ function mapToBooleanHasAnyOrganizations() { return map((orgs) => orgs.length > 0); } -export class DefaultvNextOrganizationService - implements vNextInternalOrganizationServiceAbstraction -{ +export class DefaultOrganizationService implements InternalOrganizationServiceAbstraction { memberOrganizations$(userId: UserId): Observable { return this.organizations$(userId).pipe(mapToExcludeProviderOrganizations()); } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index b3fd11982b8..598bb2a29db 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; import { ApiService } from "../../../abstractions/api.service"; import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; @@ -14,6 +13,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; +import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; diff --git a/libs/common/src/admin-console/services/organization/organization.service.spec.ts b/libs/common/src/admin-console/services/organization/organization.service.spec.ts deleted file mode 100644 index 6d2525966bc..00000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.spec.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { Utils } from "../../../platform/misc/utils"; -import { OrganizationId, UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -import { OrganizationService, ORGANIZATIONS } from "./organization.service"; - -describe("OrganizationService", () => { - let organizationService: OrganizationService; - - const fakeUserId = Utils.newGuid() as UserId; - let fakeAccountService: FakeAccountService; - let fakeStateProvider: FakeStateProvider; - let fakeActiveUserState: FakeActiveUserState>; - - /** - * It is easier to read arrays than records in code, but we store a record - * in state. This helper methods lets us build organization arrays in tests - * and easily map them to records before storing them in state. - */ - function arrayToRecord(input: OrganizationData[]): Record { - if (input == null) { - return undefined; - } - return Object.fromEntries(input?.map((i) => [i.id, i])); - } - - /** - * There are a few assertions in this spec that check for array equality - * but want to ignore a specific index that _should_ be different. This - * function takes two arrays, and an index. It checks for equality of the - * arrays, but splices out the specified index from both arrays first. - */ - function expectIsEqualExceptForIndex(x: any[], y: any[], indexToExclude: number) { - // Clone the arrays to avoid modifying the reference values - const a = [...x]; - const b = [...y]; - delete a[indexToExclude]; - delete b[indexToExclude]; - expect(a).toEqual(b); - } - - /** - * Builds a simple mock `OrganizationData[]` array that can be used in tests - * to populate state. - * @param count The number of organizations to populate the list with. The - * function returns undefined if this is less than 1. The default value is 1. - * @param suffix A string to append to data fields on each organization. - * This defaults to the index of the organization in the list. - * @returns an `OrganizationData[]` array that can be used to populate - * stateProvider. - */ - function buildMockOrganizations(count = 1, suffix?: string): OrganizationData[] { - if (count < 1) { - return undefined; - } - - function buildMockOrganization(id: OrganizationId, name: string, identifier: string) { - const data = new OrganizationData({} as any, {} as any); - data.id = id; - data.name = name; - data.identifier = identifier; - - return data; - } - - const mockOrganizations = []; - for (let i = 0; i < count; i++) { - const s = suffix ? suffix + i.toString() : i.toString(); - mockOrganizations.push( - buildMockOrganization(("org" + s) as OrganizationId, "org" + s, "orgIdentifier" + s), - ); - } - - return mockOrganizations; - } - - /** - * `OrganizationService` deals with multiple accounts at times. This helper - * function can be used to add a new non-active account to the test data. - * This function is **not** needed to handle creation of the first account, - * as that is handled by the `FakeAccountService` in `mockAccountServiceWith()` - * @returns The `UserId` of the newly created state account and the mock data - * created for them as an `Organization[]`. - */ - async function addNonActiveAccountToStateProvider(): Promise<[UserId, OrganizationData[]]> { - const nonActiveUserId = Utils.newGuid() as UserId; - - const mockOrganizations = buildMockOrganizations(10); - const fakeNonActiveUserState = fakeStateProvider.singleUser.getFake( - nonActiveUserId, - ORGANIZATIONS, - ); - fakeNonActiveUserState.nextState(arrayToRecord(mockOrganizations)); - - return [nonActiveUserId, mockOrganizations]; - } - - beforeEach(async () => { - fakeAccountService = mockAccountServiceWith(fakeUserId); - fakeStateProvider = new FakeStateProvider(fakeAccountService); - fakeActiveUserState = fakeStateProvider.activeUser.getFake(ORGANIZATIONS); - organizationService = new OrganizationService(fakeStateProvider); - }); - - it("getAll", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const orgs = await organizationService.getAll(); - expect(orgs).toHaveLength(1); - const org = orgs[0]; - expect(org).toEqual(new Organization(mockData[0])); - }); - - describe("canManageSponsorships", () => { - it("can because one is available", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipAvailable = true; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can because one is used", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = "Something"; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can not because one isn't available or taken", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(false); - }); - }); - - describe("get", () => { - it("exists", async () => { - const mockData = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.get(mockData[0].id); - expect(result).toEqual(new Organization(mockData[0])); - }); - - it("does not exist", async () => { - const result = await organizationService.get("this-org-does-not-exist"); - expect(result).toBe(undefined); - }); - }); - - describe("organizations$", () => { - describe("null checking behavior", () => { - it("publishes an empty array if organizations in state = undefined", async () => { - const mockData: OrganizationData[] = undefined; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = null", async () => { - const mockData: OrganizationData[] = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = []", async () => { - const mockData: OrganizationData[] = []; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - }); - - describe("parameter handling & returns", () => { - it("publishes all organizations for the active user by default", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData); - }); - - it("can be used to publish the organizations of a non active user if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserState"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result).toEqual(nonActiveUserMockOrganizations); - expect(result).not.toEqual(await firstValueFrom(organizationService.organizations$)); - }); - }); - }); - - describe("upsert()", () => { - it("can create the organization list if necassary", async () => { - // Notice that no default state is provided in this test, so the list in - // `stateProvider` will be null when the `upsert` method is called. - const mockData = buildMockOrganizations(); - await organizationService.upsert(mockData[0]); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData.map((x) => new Organization(x))); - }); - - it("updates an organization that already exists in state, defaulting to the active user", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: mockData[indexToUpdate].id, - }; - await organizationService.upsert(anUpdatedOrganization); - const result = await firstValueFrom(organizationService.organizations$); - expect(result[indexToUpdate]).not.toEqual(new Organization(mockData[indexToUpdate])); - expect(result[indexToUpdate].id).toEqual(new Organization(mockData[indexToUpdate]).id); - expectIsEqualExceptForIndex( - result, - mockData.map((x) => new Organization(x)), - indexToUpdate, - ); - }); - - it("can also update an organization in state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: nonActiveUserMockOrganizations[indexToUpdate].id, - }; - - await organizationService.upsert(anUpdatedOrganization, nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result[indexToUpdate]).not.toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]), - ); - expect(result[indexToUpdate].id).toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]).id, - ); - expectIsEqualExceptForIndex( - result, - nonActiveUserMockOrganizations.map((x) => new Organization(x)), - indexToUpdate, - ); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); - - describe("replace()", () => { - it("replaces the entire organization list in state, defaulting to the active user", async () => { - const originalData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - - const newData = buildMockOrganizations(10, "newData"); - await organizationService.replace(arrayToRecord(newData)); - - const result = await firstValueFrom(organizationService.organizations$); - - expect(result).toEqual(newData); - expect(result).not.toEqual(originalData); - }); - - // This is more or less a test for logouts - it("can replace state with null", async () => { - const originalData = buildMockOrganizations(2); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - await organizationService.replace(null); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - expect(result).not.toEqual(originalData); - }); - - it("can also replace state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, originalOrganizations] = await addNonActiveAccountToStateProvider(); - const newData = buildMockOrganizations(10, "newData"); - - await organizationService.replace(arrayToRecord(newData), nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - expect(result).toEqual(newData); - expect(result).not.toEqual(originalOrganizations); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); -}); diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts deleted file mode 100644 index 49e906bdac2..00000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable, firstValueFrom } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { ORGANIZATIONS_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -/** - * The `KeyDefinition` for accessing organization lists in application state. - * @todo Ideally this wouldn't require a `fromJSON()` call, but `OrganizationData` - * has some properties that contain functions. This should probably get - * cleaned up. - */ -export const ORGANIZATIONS = UserKeyDefinition.record( - ORGANIZATIONS_DISK, - "organizations", - { - deserializer: (obj: Jsonify) => OrganizationData.fromJSON(obj), - clearOn: ["logout"], - }, -); - -/** - * Filter out organizations from an observable that __do not__ offer a - * families-for-enterprise sponsorship to members. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeOrganizationsWithoutFamilySponsorshipSupport() { - return map((orgs) => orgs.filter((o) => o.canManageSponsorships)); -} - -/** - * Filter out organizations from an observable that the organization user - * __is not__ a direct member of. This will exclude organizations only - * accessible as a provider. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeProviderOrganizations() { - return map((orgs) => orgs.filter((o) => o.isMember)); -} - -/** - * Map an observable stream of organizations down to a boolean indicating - * if any organizations exist (`orgs.length > 0`). - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToBooleanHasAnyOrganizations() { - return map((orgs) => orgs.length > 0); -} - -/** - * Map an observable stream of organizations down to a single organization. - * @param `organizationId` The ID of the organization you'd like to subscribe to - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToSingleOrganization(organizationId: string) { - return map((orgs) => orgs?.find((o) => o.id === organizationId)); -} - -export class OrganizationService implements InternalOrganizationServiceAbstraction { - organizations$: Observable = this.getOrganizationsFromState$(); - memberOrganizations$: Observable = this.organizations$.pipe( - mapToExcludeProviderOrganizations(), - ); - - constructor(private stateProvider: StateProvider) {} - - get$(id: string): Observable { - return this.organizations$.pipe(mapToSingleOrganization(id)); - } - - getAll$(userId?: UserId): Observable { - return this.getOrganizationsFromState$(userId); - } - - async getAll(userId?: string): Promise { - return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); - } - - canManageSponsorships$ = this.organizations$.pipe( - mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), - mapToBooleanHasAnyOrganizations(), - ); - - familySponsorshipAvailable$ = this.organizations$.pipe( - map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)), - ); - - async hasOrganizations(): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations())); - } - - async upsert(organization: OrganizationData, userId?: UserId): Promise { - await this.stateFor(userId).update((existingOrganizations) => { - const organizations = existingOrganizations ?? {}; - organizations[organization.id] = organization; - return organizations; - }); - } - - async get(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - /** - * @deprecated For the CLI only - * @param id id of the organization - */ - async getFromState(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - async replace(organizations: { [id: string]: OrganizationData }, userId?: UserId): Promise { - await this.stateFor(userId).update(() => organizations); - } - - // Ideally this method would be renamed to organizations$() and the - // $organizations observable as it stands would be removed. This will - // require updates to callers, and so this method exists as a temporary - // workaround until we have time & a plan to update callers. - // - // It can be thought of as "organizations$ but with a userId option". - private getOrganizationsFromState$(userId?: UserId): Observable { - return this.stateFor(userId).state$.pipe(this.mapOrganizationRecordToArray()); - } - - /** - * Accepts a record of `OrganizationData`, which is how we store the - * organization list as a JSON object on disk, to an array of - * `Organization`, which is how the data is published to callers of the - * service. - * @returns a function that can be used to pipe organization data from - * stored state to an exposed object easily consumable by others. - */ - private mapOrganizationRecordToArray() { - return map, Organization[]>((orgs) => - Object.values(orgs ?? {})?.map((o) => new Organization(o)), - ); - } - - /** - * Fetches the organization list from on disk state for the specified user. - * @param userId the user ID to fetch the organization list for. Defaults to - * the currently active user. - * @returns an observable of organization state as it is stored on disk. - */ - private stateFor(userId?: UserId) { - return userId - ? this.stateProvider.getUser(userId, ORGANIZATIONS) - : this.stateProvider.getActive(ORGANIZATIONS); - } -} diff --git a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts b/libs/common/src/admin-console/services/organization/organization.state.ts similarity index 86% rename from libs/common/src/admin-console/services/organization/vnext-organization.state.ts rename to libs/common/src/admin-console/services/organization/organization.state.ts index 48e09d6d076..fea0423f389 100644 --- a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts +++ b/libs/common/src/admin-console/services/organization/organization.state.ts @@ -1,7 +1,6 @@ import { Jsonify } from "type-fest"; -import { ORGANIZATIONS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; - +import { ORGANIZATIONS_DISK, UserKeyDefinition } from "../../../platform/state"; import { OrganizationData } from "../../models/data/organization.data"; /** diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index d9802db9e38..12b57f1b4f7 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -2,8 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; +import { FakeActiveUserState, FakeSingleUserState } from "../../../../spec/fake-state"; import { OrganizationUserStatusType, OrganizationUserType, @@ -18,12 +17,14 @@ import { Policy } from "../../../admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options"; import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service"; import { PolicyId, UserId } from "../../../types/guid"; +import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; describe("PolicyService", () => { const userId = "userId" as UserId; let stateProvider: FakeStateProvider; let organizationService: MockProxy; let activeUserState: FakeActiveUserState>; + let singleUserState: FakeSingleUserState>; let policyService: PolicyService; @@ -33,6 +34,7 @@ describe("PolicyService", () => { organizationService = mock(); activeUserState = stateProvider.activeUser.getFake(POLICIES); + singleUserState = stateProvider.singleUser.getFake(activeUserState.userId, POLICIES); const organizations$ = of([ // User @@ -56,9 +58,7 @@ describe("PolicyService", () => { organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); - organizationService.organizations$ = organizations$; - - organizationService.getAll$.mockReturnValue(organizations$); + organizationService.organizations$.mockReturnValue(organizations$); policyService = new PolicyService(stateProvider, organizationService); }); @@ -196,7 +196,7 @@ describe("PolicyService", () => { describe("getResetPasswordPolicyOptions", () => { it("default", async () => { - const result = policyService.getResetPasswordPolicyOptions(null, null); + const result = policyService.getResetPasswordPolicyOptions([], ""); expect(result).toEqual([new ResetPasswordPolicyOptions(), false]); }); @@ -297,7 +297,7 @@ describe("PolicyService", () => { describe("getAll$", () => { it("returns the specified PolicyTypes", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -307,7 +307,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -333,7 +333,7 @@ describe("PolicyService", () => { }); it("does not return disabled policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -343,7 +343,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -363,7 +363,7 @@ describe("PolicyService", () => { }); it("does not return policies that do not apply to the user because the user's role is exempt", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -373,7 +373,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -393,7 +393,7 @@ describe("PolicyService", () => { }); it("does not return policies for organizations that do not use policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -403,7 +403,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 7a04ba38aa7..3378d2021ef 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { PolicyId, UserId } from "../../../types/guid"; @@ -39,7 +39,11 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.organizations$]).pipe( + const organizations$ = this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.organizationService.organizations$(userId)), + ); + + return combineLatest([filteredPolicies$, organizations$]).pipe( map( ([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)?.at(0) ?? null, @@ -47,13 +51,13 @@ export class PolicyService implements InternalPolicyServiceAbstraction { ); } - getAll$(policyType: PolicyType, userId?: UserId) { + getAll$(policyType: PolicyType, userId: UserId) { const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe( map((policyData) => policyRecordToArray(policyData)), map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.getAll$(userId)]).pipe( + return combineLatest([filteredPolicies$, this.organizationService.organizations$(userId)]).pipe( map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), ); } diff --git a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 13963b03bea..24a5d4e8413 100644 --- a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -9,7 +9,18 @@ import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "./devices/responses/device.response"; export abstract class DeviceTrustServiceAbstraction { + /** + * @deprecated - use supportsDeviceTrustByUserId instead as active user state is being deprecated + * by Platform + * @description Checks if the device trust feature is supported for the active user. + */ supportsDeviceTrust$: Observable; + + /** + * @description Checks if the device trust feature is supported for the given user. + */ + supportsDeviceTrustByUserId$: (userId: UserId) => Observable; + /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts index 707616744ad..84a2fb03c28 100644 --- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts @@ -1,6 +1,11 @@ import { DeviceType } from "../../../../enums"; import { BaseResponse } from "../../../../models/response/base.response"; +export interface DevicePendingAuthRequest { + id: string; + creationDate: string; +} + export class DeviceResponse extends BaseResponse { id: string; userId: string; @@ -10,7 +15,7 @@ export class DeviceResponse extends BaseResponse { creationDate: string; revisionDate: string; isTrusted: boolean; - devicePendingAuthRequest: { id: string; creationDate: string } | null; + devicePendingAuthRequest: DevicePendingAuthRequest | null; constructor(response: any) { super(response); diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 1c176c2b84b..fdc8c963a1b 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -22,6 +22,7 @@ export class AuthResult { ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; + requiresDeviceVerification: boolean; get requiresCaptcha() { return !Utils.isNullOrWhitespace(this.captchaSiteKey); diff --git a/libs/common/src/auth/models/request/identity-token/password-token.request.ts b/libs/common/src/auth/models/request/identity-token/password-token.request.ts index 456e058a234..3fe466e143b 100644 --- a/libs/common/src/auth/models/request/identity-token/password-token.request.ts +++ b/libs/common/src/auth/models/request/identity-token/password-token.request.ts @@ -13,6 +13,7 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect public captchaResponse: string, protected twoFactor: TokenTwoFactorRequest, device?: DeviceRequest, + public newDeviceOtp?: string, ) { super(twoFactor, device); } @@ -28,6 +29,10 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect obj.captchaResponse = this.captchaResponse; } + if (this.newDeviceOtp) { + obj.newDeviceOtp = this.newDeviceOtp; + } + return obj; } diff --git a/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts b/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts index 791c4688078..84103fe5d29 100644 --- a/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts +++ b/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts @@ -1,8 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { RotateableKeySet } from "../../../../../auth/src/common/models"; +import { EncString } from "../../../platform/models/domain/enc-string"; export class WebauthnRotateCredentialRequest { id: string; diff --git a/libs/common/src/auth/models/response/identity-device-verification.response.ts b/libs/common/src/auth/models/response/identity-device-verification.response.ts new file mode 100644 index 00000000000..b45f47e99e1 --- /dev/null +++ b/libs/common/src/auth/models/response/identity-device-verification.response.ts @@ -0,0 +1,13 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class IdentityDeviceVerificationResponse extends BaseResponse { + deviceVerified: boolean; + captchaToken: string; + + constructor(response: any) { + super(response); + this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false; + + this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); + } +} diff --git a/libs/common/src/auth/models/response/identity-response.ts b/libs/common/src/auth/models/response/identity-response.ts new file mode 100644 index 00000000000..26503a9cc2f --- /dev/null +++ b/libs/common/src/auth/models/response/identity-response.ts @@ -0,0 +1,8 @@ +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; +import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; + +export type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse; diff --git a/libs/common/src/auth/services/anonymous-hub.service.ts b/libs/common/src/auth/services/anonymous-hub.service.ts index a268c8a2712..3900dd53ee0 100644 --- a/libs/common/src/auth/services/anonymous-hub.service.ts +++ b/libs/common/src/auth/services/anonymous-hub.service.ts @@ -9,6 +9,8 @@ import { import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AuthRequestServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { NotificationType } from "../../enums"; import { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 5663384714d..4b7b8a2b262 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index bab83fc55db..d855a1be34f 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -11,6 +11,8 @@ import { switchMap, } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { StateService } from "../../platform/abstractions/state.service"; diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index 409150552ad..15c12b7a39a 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -4,6 +4,8 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; @@ -79,7 +81,17 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { private configService: ConfigService, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( - map((options) => options?.trustedDeviceOption != null ?? false), + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), + ); + } + + supportsDeviceTrustByUserId$(userId: UserId): Observable { + return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), ); } @@ -335,6 +347,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); return new SymmetricCryptoKey(userKey) as UserKey; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // If either decryption effort fails, we want to remove the device key this.logService.error("Failed to decrypt using device key. Removing device key."); diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 66a91a693e5..9f344e203c9 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -1,9 +1,15 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState } from "../../../spec/fake-state"; @@ -70,17 +76,56 @@ describe("deviceTrustService", () => { userId: mockUserId, }; + let userDecryptionOptions: UserDecryptionOptions; + beforeEach(() => { jest.clearAllMocks(); const supportsSecureStorage = false; // default to false; tests will override as needed // By default all the tests will have a mocked active user in state provider. deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); + + userDecryptionOptions = new UserDecryptionOptions(); }); it("instantiates", () => { expect(deviceTrustService).not.toBeFalsy(); }); + describe("supportsDeviceTrustByUserId$", () => { + it("returns true when the user has a non-null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: false, + isTdeOffboarding: false, + }; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(true); + }); + + it("returns false when the user has a null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = null; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(false); + }); + }); + describe("User Trust Device Choice For Decryption", () => { describe("getShouldTrustDevice", () => { it("gets the user trust device choice for decryption", async () => { diff --git a/libs/common/src/auth/services/devices/devices.service.implementation.ts b/libs/common/src/auth/services/devices/devices.service.implementation.ts index cd6f1148dd8..cdaa7a9fc4e 100644 --- a/libs/common/src/auth/services/devices/devices.service.implementation.ts +++ b/libs/common/src/auth/services/devices/devices.service.implementation.ts @@ -1,8 +1,7 @@ import { Observable, defer, map } from "rxjs"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; - import { ListResponse } from "../../../models/response/list.response"; +import { AppIdService } from "../../../platform/abstractions/app-id.service"; import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction"; import { DeviceResponse } from "../../abstractions/devices/responses/device.response"; import { DeviceView } from "../../abstractions/devices/views/device.view"; diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index b1bf87693c1..843ac383013 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,9 +1,13 @@ import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; + +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { Organization } from "../../admin-console/models/domain/organization"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; @@ -93,7 +97,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(true, true, "https://other-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -108,7 +112,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(false, false, "https://key-connector-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -123,7 +127,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 0, false), organizationData(true, true, "https://key-connector-url.com", 1, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -138,7 +142,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 2, true), organizationData(false, true, "https://key-connector-url.com", 2, true), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -179,7 +183,7 @@ describe("KeyConnectorService", () => { // create organization object const data = organizationData(true, true, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); // uses KeyConnector const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); @@ -193,7 +197,7 @@ describe("KeyConnectorService", () => { it("should return false if the user does not need migration", async () => { tokenService.getIsExternal.mockResolvedValue(false); const data = organizationData(false, false, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); state.nextState(true); @@ -273,7 +277,7 @@ describe("KeyConnectorService", () => { const masterKey = getMockMasterKey(); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); const error = new Error("Failed to post user key to key connector"); - organizationService.getAll.mockResolvedValue([organization]); + organizationService.organizations$.mockReturnValue(of([organization])); masterPasswordService.masterKeySubject.next(masterKey); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f798413162e..f6f76579ee5 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -3,6 +3,8 @@ import { firstValueFrom } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Argon2KdfConfig, KdfConfig, @@ -12,7 +14,6 @@ import { } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; import { Organization } from "../../admin-console/models/domain/organization"; import { KeysRequest } from "../../models/request/keys.request"; @@ -28,7 +29,6 @@ import { } from "../../platform/state"; import { UserId } from "../../types/guid"; import { MasterKey } from "../../types/key"; -import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; @@ -122,7 +122,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async getManagingOrganization(userId?: UserId): Promise { - const orgs = await this.organizationService.getAll(userId); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); return orgs.find( (o) => o.keyConnectorEnabled && diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts index ea6e1045c10..14e7522a836 100644 --- a/libs/common/src/auth/services/master-password/master-password.service.ts +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable } from "rxjs"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; - import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; +import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EncryptionType } from "../../../platform/enums"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; @@ -180,10 +179,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr let decUserKey: Uint8Array; if (userKey.encryptionType === EncryptionType.AesCbc256_B64) { - decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey); + decUserKey = await this.encryptService.decryptToBytes( + userKey, + masterKey, + "Content: User Key; Encrypting Key: Master Key", + ); } else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { const newKey = await this.keyGenerationService.stretchKey(masterKey); - decUserKey = await this.encryptService.decryptToBytes(userKey, newKey); + decUserKey = await this.encryptService.decryptToBytes( + userKey, + newKey, + "Content: User Key; Encrypting Key: Stretched Master Key", + ); } else { throw new Error("Unsupported encryption type."); } diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index ed622b21c86..ddd24ae7907 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -2,13 +2,15 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { UserId } from "../../../../common/src/types/guid"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; +import { UserId } from "../../types/guid"; import { Account, AccountInfo, AccountService } from "../abstractions/account.service"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index c5451ce30c6..22d5384e6ac 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -6,10 +6,12 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { Utils } from "../../platform/misc/utils"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index 081dafb1706..102e4bac8da 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -14,6 +14,8 @@ import { KeyService, } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 2935c1958a4..4735da32b6b 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -10,6 +10,8 @@ import { KeyService, } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { HashPurpose } from "../../../platform/enums"; @@ -161,6 +163,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti const request = new VerifyOTPRequest(verification.secret); try { await this.userVerificationApiService.postAccountVerifyOTP(request); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(this.i18nService.t("invalidVerificationCode")); } @@ -219,6 +223,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti request.masterPasswordHash = serverKeyHash; try { policyOptions = await this.userVerificationApiService.postAccountVerifyPassword(request); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(this.i18nService.t("invalidMasterPassword")); } diff --git a/libs/common/src/autofill/services/domain-settings.service.spec.ts b/libs/common/src/autofill/services/domain-settings.service.spec.ts index a25653f168c..36f7d0eacec 100644 --- a/libs/common/src/autofill/services/domain-settings.service.spec.ts +++ b/libs/common/src/autofill/services/domain-settings.service.spec.ts @@ -1,9 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; @@ -29,7 +28,7 @@ describe("DefaultDomainSettingsService", () => { jest.spyOn(domainSettingsService, "getUrlEquivalentDomains"); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); - domainSettingsService.blockedInteractionsUris$ = of(null); + domainSettingsService.blockedInteractionsUris$ = of({}); }); describe("getUrlEquivalentDomains", () => { diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index aeb3af69dae..b2833b9ee25 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -2,8 +2,7 @@ // @ts-strict-ignore import { map, Observable, switchMap, of } from "rxjs"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { NeverDomains, EquivalentDomains, @@ -36,7 +35,7 @@ const BLOCKED_INTERACTIONS_URIS = new KeyDefinition( DOMAIN_SETTINGS_DISK, "blockedInteractionsUris", { - deserializer: (value: NeverDomains) => value ?? null, + deserializer: (value: NeverDomains) => value ?? {}, }, ); @@ -131,7 +130,7 @@ export class DefaultDomainSettingsService implements DomainSettingsService { switchMap((featureIsEnabled) => featureIsEnabled ? this.blockedInteractionsUrisState.state$ : of({} as NeverDomains), ), - map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : null)), + map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : {})), ); this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS); diff --git a/libs/common/src/autofill/utils.spec.ts b/libs/common/src/autofill/utils.spec.ts index 4dd36ba7d89..554dc973b48 100644 --- a/libs/common/src/autofill/utils.spec.ts +++ b/libs/common/src/autofill/utils.spec.ts @@ -1,9 +1,6 @@ -import { - normalizeExpiryYearFormat, - isCardExpired, - parseYearMonthExpiry, -} from "@bitwarden/common/autofill/utils"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CardView } from "../vault/models/view/card.view"; + +import { normalizeExpiryYearFormat, isCardExpired, parseYearMonthExpiry } from "./utils"; function getExpiryYearValueFormats(currentCentury: string) { return [ diff --git a/libs/common/src/autofill/utils.ts b/libs/common/src/autofill/utils.ts index 6bee5e1a198..a77ea8a715d 100644 --- a/libs/common/src/autofill/utils.ts +++ b/libs/common/src/autofill/utils.ts @@ -1,11 +1,12 @@ +import { CardView } from "../vault/models/view/card.view"; + import { DelimiterPatternExpression, ExpiryFullYearPattern, ExpiryFullYearPatternExpression, IrrelevantExpiryCharactersPatternExpression, MonthPatternExpression, -} from "@bitwarden/common/autofill/constants"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +} from "./constants"; type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`; diff --git a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts index 06400081e11..e0e8b7377c5 100644 --- a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts @@ -3,7 +3,7 @@ import { BillingInvoiceResponse, BillingTransactionResponse, -} from "@bitwarden/common/billing/models/response/billing.response"; +} from "../../models/response/billing.response"; export class AccountBillingApiServiceAbstraction { getBillingInvoices: (status?: string, startAfter?: string) => Promise; diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 4b08b52a136..928f65a3636 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,20 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; -import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; +import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; +import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; +import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; +import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; +import { InvoicesResponse } from "../models/response/invoices.response"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 7c4e0a39f8f..69309014fac 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,11 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; +import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { name: string; @@ -46,9 +45,7 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - getPaymentSource: ( - organizationId: string, - ) => Promise; + getPaymentSource: (organizationId: string) => Promise; purchaseSubscription: (subscription: SubscriptionInformation) => Promise; diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts index 6a72724a5ec..2ed25491049 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -3,7 +3,7 @@ import { BillingInvoiceResponse, BillingTransactionResponse, -} from "@bitwarden/common/billing/models/response/billing.response"; +} from "../../models/response/billing.response"; export class OrganizationBillingApiServiceAbstraction { getBillingInvoices: ( diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts index 438d3f394e0..7a744dae856 100644 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/tax.service.abstraction.ts @@ -1,7 +1,7 @@ -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; +import { CountryListItem } from "../models/domain"; +import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; export abstract class TaxServiceAbstraction { abstract getCountries(): CountryListItem[]; diff --git a/libs/common/src/billing/models/domain/tax-information.ts b/libs/common/src/billing/models/domain/tax-information.ts index 78e1bcc42b5..794cdef3ed4 100644 --- a/libs/common/src/billing/models/domain/tax-information.ts +++ b/libs/common/src/billing/models/domain/tax-information.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; +import { TaxInfoResponse } from "../response/tax-info.response"; export class TaxInformation { country: string; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index 784d2691629..83b254ac512 100644 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information"; +import { TaxInformation } from "../domain/tax-information"; import { TaxInfoUpdateRequest } from "./tax-info-update.request"; diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts index 365dff5c110..40d8db03d3b 100644 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -1,4 +1,4 @@ -import { PlanType } from "@bitwarden/common/billing/enums"; +import { PlanType } from "../../enums"; export class PreviewOrganizationInvoiceRequest { organizationId?: string; diff --git a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts index c740e4157ed..e4bf575cc6a 100644 --- a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts +++ b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { PaymentMethodType } from "../../enums"; export class TokenizedPaymentSourceRequest { type: PaymentMethodType; diff --git a/libs/common/src/billing/models/request/update-payment-method.request.ts b/libs/common/src/billing/models/request/update-payment-method.request.ts index 9ef91ae579b..10b03103716 100644 --- a/libs/common/src/billing/models/request/update-payment-method.request.ts +++ b/libs/common/src/billing/models/request/update-payment-method.request.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; +import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; +import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request"; export class UpdatePaymentMethodRequest { paymentSource: TokenizedPaymentSourceRequest; diff --git a/libs/common/src/billing/models/response/invoices.response.ts b/libs/common/src/billing/models/response/invoices.response.ts index bf797ba42d6..05c95b83c6f 100644 --- a/libs/common/src/billing/models/response/invoices.response.ts +++ b/libs/common/src/billing/models/response/invoices.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class InvoicesResponse extends BaseResponse { invoices: InvoiceResponse[] = []; diff --git a/libs/common/src/billing/models/response/payment-source.response.ts b/libs/common/src/billing/models/response/payment-source.response.ts index 1aeeb450b11..93418fc2f55 100644 --- a/libs/common/src/billing/models/response/payment-source.response.ts +++ b/libs/common/src/billing/models/response/payment-source.response.ts @@ -1,5 +1,5 @@ -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; +import { PaymentMethodType } from "../../enums"; export class PaymentSourceResponse extends BaseResponse { type: PaymentMethodType; diff --git a/libs/common/src/billing/models/response/preview-invoice.response.ts b/libs/common/src/billing/models/response/preview-invoice.response.ts index c822a569bb3..efd3da3e9f1 100644 --- a/libs/common/src/billing/models/response/preview-invoice.response.ts +++ b/libs/common/src/billing/models/response/preview-invoice.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class PreviewInvoiceResponse extends BaseResponse { effectiveTaxRate: number; diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts index 2ecf988addd..4481f7588ff 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -1,9 +1,9 @@ -import { ProviderType } from "@bitwarden/common/admin-console/enums"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { SubscriptionSuspensionResponse } from "@bitwarden/common/billing/models/response/subscription-suspension.response"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - +import { ProviderType } from "../../../admin-console/enums"; import { BaseResponse } from "../../../models/response/base.response"; +import { PlanType, ProductTierType } from "../../enums"; + +import { SubscriptionSuspensionResponse } from "./subscription-suspension.response"; +import { TaxInfoResponse } from "./tax-info.response"; export class ProviderSubscriptionResponse extends BaseResponse { status: string; diff --git a/libs/common/src/billing/models/response/subscription-suspension.response.ts b/libs/common/src/billing/models/response/subscription-suspension.response.ts index 418e1c443c8..3d714a05dba 100644 --- a/libs/common/src/billing/models/response/subscription-suspension.response.ts +++ b/libs/common/src/billing/models/response/subscription-suspension.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class SubscriptionSuspensionResponse extends BaseResponse { suspensionDate: string; diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts index 0d5cce46c8c..f31f2133b34 100644 --- a/libs/common/src/billing/models/response/tax-id-types.response.ts +++ b/libs/common/src/billing/models/response/tax-id-types.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class TaxIdTypesResponse extends BaseResponse { taxIdTypes: TaxIdTypeResponse[] = []; diff --git a/libs/common/src/billing/models/response/tax-rate.response.ts b/libs/common/src/billing/models/response/tax-rate.response.ts deleted file mode 100644 index 2c07129ba2c..00000000000 --- a/libs/common/src/billing/models/response/tax-rate.response.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -export class TaxRateResponse extends BaseResponse { - id: string; - country: string; - state: string; - postalCode: string; - rate: number; - - constructor(response: any) { - super(response); - this.id = this.getResponseProperty("Id"); - this.country = this.getResponseProperty("Country"); - this.state = this.getResponseProperty("State"); - this.postalCode = this.getResponseProperty("PostalCode"); - this.rate = this.getResponseProperty("Rate"); - } -} diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 372d8099865..02dbef469d6 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -1,17 +1,16 @@ import { firstValueFrom } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - import { FakeAccountService, mockAccountServiceWith, FakeStateProvider, FakeSingleUserState, } from "../../../../spec"; +import { ApiService } from "../../../abstractions/api.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service"; +import { BillingHistoryResponse } from "../../models/response/billing-history.response"; import { BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index 579a81eeb5c..155ce1493b4 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -1,8 +1,7 @@ import { map, Observable, combineLatest, concatMap } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - +import { ApiService } from "../../../abstractions/api.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { BILLING_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 7ce5602f3cc..4306324395e 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,25 +1,25 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; -import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ToastService } from "@bitwarden/components"; import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; -import { BillingApiServiceAbstraction } from "../../billing/abstractions"; -import { PaymentMethodType } from "../../billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request"; -import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; -import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; -import { PlanResponse } from "../../billing/models/response/plan.response"; +import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; +import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; +import { LogService } from "../../platform/abstractions/log.service"; +import { BillingApiServiceAbstraction } from "../abstractions"; +import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; +import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; +import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; +import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; +import { InvoicesResponse } from "../models/response/invoices.response"; +import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; +import { PlanResponse } from "../models/response/plan.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index ca10b368662..e61b092d677 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,18 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - BillingApiServiceAbstraction, - OrganizationBillingServiceAbstraction, - OrganizationInformation, - PaymentInformation, - PlanInformation, - SubscriptionInformation, -} from "@bitwarden/common/billing/abstractions"; -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -23,9 +10,19 @@ import { OrganizationResponse } from "../../admin-console/models/response/organi import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { SyncService } from "../../platform/sync"; import { OrgKey } from "../../types/key"; +import { + BillingApiServiceAbstraction, + OrganizationBillingServiceAbstraction, + OrganizationInformation, + PaymentInformation, + PlanInformation, + SubscriptionInformation, +} from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; +import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { encryptedKey: EncString; @@ -38,7 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs constructor( private apiService: ApiService, private billingApiService: BillingApiServiceAbstraction, - private configService: ConfigService, private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, @@ -46,21 +42,9 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async getPaymentSource( - organizationId: string, - ): Promise { - const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - - if (deprecateStripeSourcesAPI) { - const paymentMethod = - await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod.paymentSource; - } else { - const billing = await this.organizationApiService.getBilling(organizationId); - return billing.paymentSource; - } + async getPaymentSource(organizationId: string): Promise { + const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); + return paymentMethod.paymentSource; } async purchaseSubscription(subscription: SubscriptionInformation): Promise { diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts index 45e57267ec0..aa27c99adc8 100644 --- a/libs/common/src/billing/services/tax.service.ts +++ b/libs/common/src/billing/services/tax.service.ts @@ -1,9 +1,9 @@ -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; +import { ApiService } from "../../abstractions/api.service"; +import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; +import { CountryListItem } from "../models/domain"; +import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; export class TaxService implements TaxServiceAbstraction { constructor(private apiService: ApiService) {} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0ab7d47acfc..a988bdbf6a7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -4,47 +4,49 @@ * Flags MUST be short lived and SHALL be removed once enabled. */ export enum FeatureFlag { - BrowserFilelessImport = "browser-fileless-import", + /* Admin Console Team */ + ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", + AccountDeprovisioning = "pm-10308-account-deprovisioning", + VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", + PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", + + /* Autofill */ + BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", + DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", + EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", + GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", + IdpAutoSubmitLogin = "idp-auto-submit-login", + InlineMenuFieldQualification = "inline-menu-field-qualification", + InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", + InlineMenuTotp = "inline-menu-totp", + NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", + NotificationRefresh = "notification-refresh", + UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", + ItemShare = "item-share", GeneratorToolsModernization = "generator-tools-modernization", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", - UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", - EmailVerification = "email-verification", - InlineMenuFieldQualification = "inline-menu-field-qualification", TwoFactorComponentRefactor = "two-factor-component-refactor", - InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", - ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", - IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", - GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", - EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", - DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", - AccountDeprovisioning = "pm-10308-account-deprovisioning", SSHKeyVaultItem = "ssh-key-vault-item", SSHAgent = "ssh-agent", - NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", - BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", - AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", - VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", - PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", SecurityTasks = "security-tasks", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", - InlineMenuTotp = "inline-menu-totp", MacOsNativeCredentialSync = "macos-native-credential-sync", - PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", - PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", + PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs", + NewDeviceVerification = "new-device-verification", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -59,47 +61,49 @@ const FALSE = false as boolean; * We support true as a value as we prefer flags to "enable" not "disable". */ export const DefaultFeatureFlagValue = { - [FeatureFlag.BrowserFilelessImport]: FALSE, + /* Admin Console Team */ + [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, + [FeatureFlag.AccountDeprovisioning]: FALSE, + [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, + [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, + + /* Autofill */ + [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, + [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, + [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, + [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, + [FeatureFlag.IdpAutoSubmitLogin]: FALSE, + [FeatureFlag.InlineMenuFieldQualification]: FALSE, + [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, + [FeatureFlag.InlineMenuTotp]: FALSE, + [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, + [FeatureFlag.NotificationRefresh]: FALSE, + [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, + [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.GeneratorToolsModernization]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, - [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, - [FeatureFlag.EmailVerification]: FALSE, - [FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE, - [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, - [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, - [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, - [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, - [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, - [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, - [FeatureFlag.AccountDeprovisioning]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE, [FeatureFlag.SSHAgent]: FALSE, - [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, - [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, - [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, - [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, - [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, - [FeatureFlag.InlineMenuTotp]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, - [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, - [FeatureFlag.PM12443RemovePagingLogic]: FALSE, + [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.ResellerManagedOrgAlert]: FALSE, + [FeatureFlag.NewDeviceVerification]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index 69cbdff9dd2..db59fcafa69 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -23,4 +23,5 @@ export enum NotificationType { SyncOrganizations = 17, SyncOrganizationStatusChanged = 18, + SyncOrganizationCollectionSettingChanged = 19, } diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts index 8c1d1117c89..9f97e0a94c1 100644 --- a/libs/common/src/key-management/services/default-process-reload.service.ts +++ b/libs/common/src/key-management/services/default-process-reload.service.ts @@ -2,16 +2,18 @@ // @ts-strict-ignore import { firstValueFrom, map, timeout } from "rxjs"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; +import { MessagingService } from "../../platform/abstractions/messaging.service"; import { UserId } from "../../types/guid"; import { ProcessReloadServiceAbstraction } from "../abstractions/process-reload.service"; @@ -37,6 +39,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); status = await authService.getAuthStatus(userId); if (status === AuthenticationStatus.Unlocked) { + this.logService.info( + "[Process Reload Service] User unlocked, preventing process reload", + ); return; } } @@ -53,6 +58,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract if (userId != null) { const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); if (ephemeralPin != null) { + this.logService.info( + "[Process Reload Service] Ephemeral pin active, preventing process reload", + ); return; } } @@ -95,7 +103,12 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract await this.reloadCallback(); } return; + } else { + this.logService.info( + "[Process Reload Service] Desktop ipc fingerprint validated, preventing process reload", + ); } + if (this.reloadInterval == null) { this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); } diff --git a/libs/common/src/models/export/ssh-key.export.ts b/libs/common/src/models/export/ssh-key.export.ts index c502ddafd47..a99ebac34b3 100644 --- a/libs/common/src/models/export/ssh-key.export.ts +++ b/libs/common/src/models/export/ssh-key.export.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { SshKeyView as SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { EncString } from "../../platform/models/domain/enc-string"; import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key"; +import { SshKeyView as SshKeyView } from "../../vault/models/view/ssh-key.view"; import { safeGetString } from "./utils"; diff --git a/libs/common/src/models/export/utils.ts b/libs/common/src/models/export/utils.ts index 630b4898503..b7e0b74b611 100644 --- a/libs/common/src/models/export/utils.ts +++ b/libs/common/src/models/export/utils.ts @@ -1,4 +1,4 @@ -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncString } from "../../platform/models/domain/enc-string"; export function safeGetString(value: string | EncString) { if (value == null) { diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 473e6fc1d10..894a00ee885 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -45,6 +45,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncOrganizationStatusChanged: this.payload = new OrganizationStatusPushNotification(payload); break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + this.payload = new OrganizationCollectionSettingChangedPushNotification(payload); + break; default: break; } @@ -126,3 +129,17 @@ export class OrganizationStatusPushNotification extends BaseResponse { this.enabled = this.getResponseProperty("Enabled"); } } + +export class OrganizationCollectionSettingChangedPushNotification extends BaseResponse { + organizationId: string; + limitCollectionCreation: boolean; + limitCollectionDeletion: boolean; + + constructor(response: any) { + super(response); + + this.organizationId = this.getResponseProperty("OrganizationId"); + this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); + this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + } +} diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index 18c14677dd0..56b0ee55afe 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -1,5 +1,5 @@ import { CsprngArray } from "../../types/csprng"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoFunctionService { @@ -51,11 +51,13 @@ export abstract class CryptoFunctionService { iv: string, mac: string, key: SymmetricCryptoKey, - ): DecryptParameters; - abstract aesDecryptFast( - parameters: DecryptParameters, - mode: "cbc" | "ecb", - ): Promise; + ): CbcDecryptParameters; + abstract aesDecryptFast({ + mode, + parameters, + }: + | { mode: "cbc"; parameters: CbcDecryptParameters } + | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise; abstract aesDecrypt( data: Uint8Array, iv: Uint8Array, diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index 5b28b98803b..a660524699d 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -8,12 +8,32 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an EncString to a string + * @param encString - The EncString to decrypt + * @param key - The key to decrypt the EncString with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted string + */ abstract decryptToUtf8( encString: EncString, key: SymmetricCryptoKey, - decryptContext?: string, + decryptTrace?: string, ): Promise; - abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an Encrypted object to a Uint8Array + * @param encThing - The Encrypted object to decrypt + * @param key - The key to decrypt the Encrypted object with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted Uint8Array + */ + abstract decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptTrace?: string, + ): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; diff --git a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts index d684561dacd..6a1b7b67b42 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts @@ -4,6 +4,10 @@ import type { BitwardenClient } from "@bitwarden/sdk-internal"; * Factory for creating SDK clients. */ export abstract class SdkClientFactory { + /** + * Creates a new BitwardenClient. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + */ abstract createSdkClient( ...args: ConstructorParameters ): Promise; diff --git a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts new file mode 100644 index 00000000000..16482e797b2 --- /dev/null +++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts @@ -0,0 +1,3 @@ +export abstract class SdkLoadService { + abstract load(): Promise; +} diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index d44d38c36ab..78ec11c4022 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { BitwardenClient } from "@bitwarden/sdk-internal"; @@ -10,13 +8,13 @@ export abstract class SdkService { /** * Retrieve the version of the SDK. */ - version$: Observable; + abstract version$: Observable; /** * Retrieve a client initialized without a user. * This client can only be used for operations that don't require a user context. */ - client$: Observable; + abstract client$: Observable; /** * Retrieve a client initialized for a specific user. @@ -29,5 +27,5 @@ export abstract class SdkService { * * @param userId */ - abstract userClient$(userId: UserId): Observable; + abstract userClient$(userId: UserId): Observable; } diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index 8ed19ce57fc..30531b6799e 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -1,14 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type SharedFlags = { sdk?: boolean; prereleaseBuild?: boolean; }; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; skipWelcomeOnInstall: boolean; diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index d2bf11a20ce..f654897e9e2 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,10 +8,14 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports const nodeURL = typeof self === "undefined" ? require("url") : null; declare global { @@ -608,6 +612,8 @@ export class Utils { } return new URL(uriString); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore error } diff --git a/libs/common/src/platform/models/domain/decrypt-parameters.ts b/libs/common/src/platform/models/domain/decrypt-parameters.ts index 784826d3bd2..d3b4bf60d42 100644 --- a/libs/common/src/platform/models/domain/decrypt-parameters.ts +++ b/libs/common/src/platform/models/domain/decrypt-parameters.ts @@ -1,10 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export class DecryptParameters { +export type CbcDecryptParameters = { encKey: T; data: T; iv: T; - macKey: T; - mac: T; + macKey?: T; + mac?: T; macData: T; -} +}; + +export type EcbDecryptParameters = { + encKey: T; + data: T; +}; diff --git a/libs/common/src/platform/models/domain/domain-base.spec.ts b/libs/common/src/platform/models/domain/domain-base.spec.ts index 0bdee21e3c1..80a4e5e8606 100644 --- a/libs/common/src/platform/models/domain/domain-base.spec.ts +++ b/libs/common/src/platform/models/domain/domain-base.spec.ts @@ -67,9 +67,13 @@ describe("DomainBase", () => { ); // @ts-expect-error -- encString2 was not decrypted + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions decrypted as { encToString: string; encString2: string; plainText: string }; // encString2 was not decrypted, so it's still an EncString + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions decrypted as { encToString: string; encString2: EncString; plainText: string }; }); diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index bd9139999b7..110a1dc7208 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -8,7 +8,7 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type type EncStringKeys = ConditionalKeys, EncString>; export type DecryptedObject< TEncryptedObject, @@ -63,6 +63,7 @@ export default class Domain { map: any, orgId: string, key: SymmetricCryptoKey = null, + objectContext: string = "No Domain Context", ): Promise { const promises = []; const self: any = this; @@ -78,7 +79,11 @@ export default class Domain { .then(() => { const mapProp = map[theProp] || theProp; if (self[mapProp]) { - return self[mapProp].decrypt(orgId, key); + return self[mapProp].decrypt( + orgId, + key, + `Property: ${prop}; ObjectContext: ${objectContext}`, + ); } return null; }) @@ -114,12 +119,21 @@ export default class Domain { key: SymmetricCryptoKey, encryptService: EncryptService, _: Constructor = this.constructor as Constructor, + objectContext: string = "No Domain Context", ): Promise> { const promises = []; for (const prop of encryptedProperties) { const value = (this as any)[prop] as EncString; - promises.push(this.decryptProperty(prop, value, key, encryptService)); + promises.push( + this.decryptProperty( + prop, + value, + key, + encryptService, + `Property: ${prop.toString()}; ObjectContext: ${objectContext}`, + ), + ); } const decryptedObjects = await Promise.all(promises); @@ -137,10 +151,11 @@ export default class Domain { value: EncString, key: SymmetricCryptoKey, encryptService: EncryptService, + decryptTrace: string, ) { let decrypted: string = null; if (value) { - decrypted = await value.decryptWithKey(key, encryptService); + decrypted = await value.decryptWithKey(key, encryptService, decryptTrace); } else { decrypted = null; } diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 85108a9609b..b4916b9f70a 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index c484c80ee5b..f148664a4f9 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -125,6 +125,8 @@ export class EncString implements Encrypted { try { encType = parseInt(headerPieces[0], null); encPieces = headerPieces[1].split("|"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return { encType: NaN, encPieces: [] }; } @@ -156,21 +158,21 @@ export class EncString implements Encrypted { return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length; } - async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise { + async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise { if (this.decryptedValue != null) { return this.decryptedValue; } - let keyContext = "provided-key"; + let decryptTrace = "provided-key"; try { if (key == null) { key = await this.getKeyForDecryption(orgId); - keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; + decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; if (orgId != null) { - keyContext = `domain-orgkey-${orgId}`; + decryptTrace = `domain-orgkey-${orgId}`; } else { const cryptoService = Utils.getContainerService().getKeyService(); - keyContext = + decryptTrace = (await cryptoService.getUserKey()) == null ? "domain-withlegacysupport-masterkey" : "domain-withlegacysupport-userkey"; @@ -181,20 +183,32 @@ export class EncString implements Encrypted { } const encryptService = Utils.getContainerService().getEncryptService(); - this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext); + this.decryptedValue = await encryptService.decryptToUtf8( + this, + key, + decryptTrace == null ? context : `${decryptTrace}${context || ""}`, + ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.decryptedValue = DECRYPT_ERROR; } return this.decryptedValue; } - async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) { + async decryptWithKey( + key: SymmetricCryptoKey, + encryptService: EncryptService, + decryptTrace: string = "domain-withkey", + ): Promise { try { if (key == null) { throw new Error("No key to decrypt EncString"); } - this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey"); + this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.decryptedValue = DECRYPT_ERROR; } diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index f467cb8d6e4..eab4c7b2114 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -7,7 +7,7 @@ import { EncryptionType } from "../../enums"; export class SymmetricCryptoKey { key: Uint8Array; - encKey?: Uint8Array; + encKey: Uint8Array; macKey?: Uint8Array; encType: EncryptionType; @@ -48,12 +48,8 @@ export class SymmetricCryptoKey { throw new Error("Unsupported encType/key length."); } - if (this.key != null) { - this.keyB64 = Utils.fromBufferToB64(this.key); - } - if (this.encKey != null) { - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - } + this.keyB64 = Utils.fromBufferToB64(this.key); + this.encKeyB64 = Utils.fromBufferToB64(this.encKey); if (this.macKey != null) { this.macKeyB64 = Utils.fromBufferToB64(this.macKey); } diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index 6022e097ab0..c3e727a2e1e 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,3 +1,5 @@ +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 0a85b34eba8..68263cadf27 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -114,7 +114,7 @@ export class EncryptServiceImplementation implements EncryptService { const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { this.logMacFailed( - "[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " + + "[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " + encryptionTypeName(key.encType) + "Payload type " + encryptionTypeName(encString.encryptionType) + @@ -125,10 +125,14 @@ export class EncryptServiceImplementation implements EncryptService { } } - return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc"); + return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams }); } - async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise { + async decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptContext: string = "no context", + ): Promise { if (key == null) { throw new Error("No encryption key provided."); } @@ -145,7 +149,9 @@ export class EncryptServiceImplementation implements EncryptService { "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -155,7 +161,9 @@ export class EncryptServiceImplementation implements EncryptService { "[Encrypt service] Key encryption type does not match payload encryption type. Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -167,11 +175,13 @@ export class EncryptServiceImplementation implements EncryptService { const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); if (computedMac === null) { this.logMacFailed( - "[Encrypt service] Failed to compute MAC." + + "[Encrypt service#decryptToBytes] Failed to compute MAC." + " Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -179,11 +189,13 @@ export class EncryptServiceImplementation implements EncryptService { const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { this.logMacFailed( - "[Encrypt service] MAC comparison failed. Key or payload has changed." + + "[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." + " Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } diff --git a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts index 8e99af2efed..fc55cc83ac8 100644 --- a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts +++ b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts @@ -1,19 +1,19 @@ import * as sdk from "@bitwarden/sdk-internal"; -import * as module from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; /** - * Directly imports the Bitwarden SDK and initializes it. - * - * **Warning**: This requires WASM support and will fail if the environment does not support it. + * Default SDK client factory. */ export class DefaultSdkClientFactory implements SdkClientFactory { + /** + * Initializes a Bitwarden client. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + * @returns A BitwardenClient + */ async createSdkClient( ...args: ConstructorParameters ): Promise { - (sdk as any).init(module); - return Promise.resolve(new sdk.BitwardenClient(...args)); } } diff --git a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts new file mode 100644 index 00000000000..eff641f0351 --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts @@ -0,0 +1,15 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as bitwardenModule from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; + +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +/** + * Directly imports the Bitwarden SDK and initializes it. + * + * **Warning**: This requires WASM support and will fail if the environment does not support it. + */ +export class DefaultSdkLoadService implements SdkLoadService { + async load(): Promise { + (sdk as any).init(bitwardenModule); + } +} diff --git a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts new file mode 100644 index 00000000000..60dac4f21f1 --- /dev/null +++ b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts @@ -0,0 +1,7 @@ +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +export class NoopSdkLoadService extends SdkLoadService { + async load() { + return; + } +} diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts index 23a8ba3138b..16b3968045a 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DefaultKeyService } from "../../../../key-management/src/key.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts index abb8993c39c..a8947a49f45 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -1,3 +1,5 @@ +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { UserId } from "../../types/guid"; import { KeySuffixOptions } from "../enums"; diff --git a/libs/common/src/platform/services/web-crypto-function.service.spec.ts b/libs/common/src/platform/services/web-crypto-function.service.spec.ts index 71f2828855f..1929e6454ef 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.spec.ts @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended"; import { Utils } from "../../platform/misc/utils"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { WebCryptoFunctionService } from "./web-crypto-function.service"; @@ -253,8 +253,13 @@ describe("WebCrypto Function Service", () => { const encData = Utils.fromBufferToB64(encValue); const b64Iv = Utils.fromBufferToB64(iv); const symKey = new SymmetricCryptoKey(key); - const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); + const parameters = cryptoFunctionService.aesDecryptFastParameters( + encData, + b64Iv, + null, + symKey, + ); + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters }); expect(decValue).toBe(value); }); @@ -276,8 +281,8 @@ describe("WebCrypto Function Service", () => { const iv = Utils.fromBufferToB64(makeStaticByteArray(16)); const symKey = new SymmetricCryptoKey(makeStaticByteArray(32)); const data = "ByUF8vhyX4ddU9gcooznwA=="; - const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); + const parameters = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters }); expect(decValue).toBe("EncryptMe!"); }); }); @@ -287,10 +292,11 @@ describe("WebCrypto Function Service", () => { const cryptoFunctionService = getWebCryptoFunctionService(); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); - const params = new DecryptParameters(); - params.encKey = Utils.fromBufferToByteString(key); - params.data = Utils.fromBufferToByteString(data); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "ecb"); + const parameters: EcbDecryptParameters = { + encKey: Utils.fromBufferToByteString(key), + data: Utils.fromBufferToByteString(data), + }; + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "ecb", parameters }); expect(decValue).toBe("EncryptMe!"); }); }); @@ -304,6 +310,15 @@ describe("WebCrypto Function Service", () => { const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "cbc"); expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); }); + + it("throws if iv is not provided", async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = makeStaticByteArray(32); + const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA=="); + await expect(() => cryptoFunctionService.aesDecrypt(data, null, key, "cbc")).rejects.toThrow( + "IV is required for CBC mode", + ); + }); }); describe("aesDecrypt ECB mode", () => { diff --git a/libs/common/src/platform/services/web-crypto-function.service.ts b/libs/common/src/platform/services/web-crypto-function.service.ts index c0592654849..61edf7a13b1 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import * as argon2 from "argon2-browser"; import * as forge from "node-forge"; import { Utils } from "../../platform/misc/utils"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export class WebCryptoFunctionService implements CryptoFunctionService { @@ -14,10 +12,14 @@ export class WebCryptoFunctionService implements CryptoFunctionService { private subtle: SubtleCrypto; private wasmSupported: boolean; - constructor(globalContext: Window | typeof global) { - this.crypto = typeof globalContext.crypto !== "undefined" ? globalContext.crypto : null; - this.subtle = - !!this.crypto && typeof this.crypto.subtle !== "undefined" ? this.crypto.subtle : null; + constructor(globalContext: { crypto: Crypto }) { + if (globalContext?.crypto?.subtle == null) { + throw new Error( + "Could not instantiate WebCryptoFunctionService. Could not locate Subtle crypto.", + ); + } + this.crypto = globalContext.crypto; + this.subtle = this.crypto.subtle; this.wasmSupported = this.checkIfWasmSupported(); } @@ -220,7 +222,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { hmac.update(a); const mac1 = hmac.digest().getBytes(); - hmac.start(null, null); + hmac.start("sha256", null); hmac.update(b); const mac2 = hmac.digest().getBytes(); @@ -239,10 +241,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { aesDecryptFastParameters( data: string, iv: string, - mac: string, + mac: string | null, key: SymmetricCryptoKey, - ): DecryptParameters { - const p = new DecryptParameters(); + ): CbcDecryptParameters { + const p = {} as CbcDecryptParameters; if (key.meta != null) { p.encKey = key.meta.encKeyByteString; p.macKey = key.meta.macKeyByteString; @@ -275,7 +277,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return p; } - aesDecryptFast(parameters: DecryptParameters, mode: "cbc" | "ecb"): Promise { + aesDecryptFast({ + mode, + parameters, + }: + | { mode: "cbc"; parameters: CbcDecryptParameters } + | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise { const decipher = (forge as any).cipher.createDecipher( this.toWebCryptoAesMode(mode), parameters.encKey, @@ -294,21 +301,27 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async aesDecrypt( data: Uint8Array, - iv: Uint8Array, + iv: Uint8Array | null, key: Uint8Array, mode: "cbc" | "ecb", ): Promise { if (mode === "ecb") { // Web crypto does not support AES-ECB mode, so we need to do this in forge. - const params = new DecryptParameters(); - params.data = this.toByteString(data); - params.encKey = this.toByteString(key); - const result = await this.aesDecryptFast(params, "ecb"); + const parameters: EcbDecryptParameters = { + data: this.toByteString(data), + encKey: this.toByteString(key), + }; + const result = await this.aesDecryptFast({ mode: "ecb", parameters }); return Utils.fromByteStringToArray(result); } const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [ "decrypt", ]); + + // CBC + if (iv == null) { + throw new Error("IV is required for CBC mode."); + } const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data); return new Uint8Array(buffer); } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts index 3c8c39e21e8..61f36fa0b75 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts @@ -8,7 +8,14 @@ import { DerivedStateProvider } from "../derived-state.provider"; import { DefaultDerivedState } from "./default-derived-state"; export class DefaultDerivedStateProvider implements DerivedStateProvider { - private cache: Record> = {}; + /** + * The cache uses a WeakMap to maintain separate derived states per user. + * Each user's state Observable acts as a unique key, without needing to + * pass around `userId`. Also, when a user's state Observable is cleaned up + * (like during an account swap) their cache is automatically garbage + * collected. + */ + private cache = new WeakMap, Record>>(); constructor() {} @@ -17,8 +24,14 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { + let stateCache = this.cache.get(parentState$); + if (!stateCache) { + stateCache = {}; + this.cache.set(parentState$, stateCache); + } + const cacheKey = deriveDefinition.buildCacheKey(); - const existingDerivedState = this.cache[cacheKey]; + const existingDerivedState = stateCache[cacheKey]; if (existingDerivedState != null) { // I have to cast out of the unknown generic but this should be safe if rules // around domain token are made @@ -26,7 +39,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { } const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies); - this.cache[cacheKey] = newDerivedState; + stateCache[cacheKey] = newDerivedState; return newDerivedState; } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts index 7e8d76bd203..6fcc1c408cb 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts @@ -9,6 +9,7 @@ import { DeriveDefinition } from "../derive-definition"; import { StateDefinition } from "../state-definition"; import { DefaultDerivedState } from "./default-derived-state"; +import { DefaultDerivedStateProvider } from "./default-derived-state.provider"; let callCount = 0; const cleanupDelayMs = 10; @@ -182,4 +183,29 @@ describe("DefaultDerivedState", () => { expect(await firstValueFrom(observable)).toEqual(new Date(newDate)); }); }); + + describe("account switching", () => { + let provider: DefaultDerivedStateProvider; + + beforeEach(() => { + provider = new DefaultDerivedStateProvider(); + }); + + it("should provide a dedicated cache for each account", async () => { + const user1State$ = new Subject(); + const user1Derived = provider.get(user1State$, deriveDefinition, deps); + const user1Emissions = trackEmissions(user1Derived.state$); + + const user2State$ = new Subject(); + const user2Derived = provider.get(user2State$, deriveDefinition, deps); + const user2Emissions = trackEmissions(user2Derived.state$); + + user1State$.next("2015-12-30"); + user2State$.next("2020-12-29"); + await awaitAsync(); + + expect(user1Emissions).toEqual([new Date("2015-12-30")]); + expect(user2Emissions).toEqual([new Date("2020-12-29")]); + }); + }); }); diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index 567de957e53..f285963d6e6 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -28,7 +28,7 @@ import { getStoredValue } from "./util"; // The parts of a KeyDefinition this class cares about to make it work type KeyDefinitionRequirements = { - deserializer: (jsonState: Jsonify) => T; + deserializer: (jsonState: Jsonify) => T | null; cleanupDelayMs: number; debug: Required; }; diff --git a/libs/common/src/platform/state/implementations/util.ts b/libs/common/src/platform/state/implementations/util.ts index f3d57fbafc4..0a9d76f6da5 100644 --- a/libs/common/src/platform/state/implementations/util.ts +++ b/libs/common/src/platform/state/implementations/util.ts @@ -5,12 +5,11 @@ import { AbstractStorageService } from "../../abstractions/storage.service"; export async function getStoredValue( key: string, storage: AbstractStorageService, - deserializer: (jsonValue: Jsonify) => T, + deserializer: (jsonValue: Jsonify) => T | null, ) { if (storage.valuesRequireDeserialization) { const jsonValue = await storage.get>(key); - const value = deserializer(jsonValue); - return value; + return deserializer(jsonValue); } else { const value = await storage.get(key); return value ?? null; diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index a270fc3e1a2..519e98ef52d 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -42,7 +42,7 @@ export type KeyDefinitionOptions = { * @param jsonValue The JSON object representation of your state. * @returns The fully typed version of your state. */ - readonly deserializer: (jsonValue: Jsonify) => T; + readonly deserializer: (jsonValue: Jsonify) => T | null; /** * The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. * Defaults to 1000ms. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 1ed5227cb13..c83119d9ad4 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -179,7 +179,6 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro web: "disk-local", }); export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk"); -export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk"); export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( "newDeviceVerificationNotice", "disk", @@ -188,3 +187,4 @@ export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( }, ); export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk"); +export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index cfa9030c9de..48f4b2b64f0 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -8,6 +8,7 @@ import { ApiService } from "../../abstractions/api.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { getUserId } from "../../auth/services/account.service"; import { SyncCipherNotification, SyncFolderNotification, @@ -58,7 +59,7 @@ export abstract class CoreSyncService implements SyncService { abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; async getLastSync(): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (userId == null) { return null; } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 9e36aa69417..7acd7dd8c7b 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { CollectionService, @@ -8,8 +8,14 @@ import { CollectionDetailsResponse, } from "@bitwarden/admin-console/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LogoutReason } from "../../../../auth/src/common/types"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; @@ -28,6 +34,7 @@ import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstraction import { TokenService } from "../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; +import { getUserId } from "../../auth/services/account.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../billing/abstractions"; import { DomainsResponse } from "../../models/response/domains.response"; @@ -101,7 +108,7 @@ export class DefaultSyncService extends CoreSyncService { @sequentialize(() => "fullSync") override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.syncStarted(); const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); if (authStatus === AuthenticationStatus.LoggedOut) { diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index dc0a8d61f64..03ea969c7bc 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -3,9 +3,9 @@ import { firstValueFrom } from "rxjs"; import { - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, + CollectionRequest, CollectionResponse, } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -78,6 +78,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -103,7 +104,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { TaxRateResponse } from "../billing/models/response/tax-rate.response"; import { DeviceType } from "../enums"; import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum"; import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request"; @@ -158,6 +158,13 @@ export class ApiService implements ApiServiceAbstraction { private deviceType: string; private isWebClient = false; private isDesktopClient = false; + private refreshTokenPromise: Promise | undefined; + + /** + * The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required. + */ + private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE = + "new device verification required"; constructor( private tokenService: TokenService, @@ -198,7 +205,12 @@ export class ApiService implements ApiServiceAbstraction { | PasswordTokenRequest | SsoTokenRequest | WebAuthnLoginTokenRequest, - ): Promise { + ): Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + > { const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", Accept: "application/json", @@ -246,6 +258,11 @@ export class ApiService implements ApiServiceAbstraction { Object.keys(responseJson.HCaptcha_SiteKey).length ) { return new IdentityCaptchaResponse(responseJson); + } else if ( + response.status === 400 && + responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE + ) { + return new IdentityDeviceVerificationResponse(responseJson); } } @@ -897,11 +914,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PlanResponse); } - async getTaxRates(): Promise> { - const r = await this.send("GET", "/plans/sales-tax-rates/", null, true, true); - return new ListResponse(r, TaxRateResponse); - } - // Settings APIs async getSettingsDomains(): Promise { @@ -1722,7 +1734,18 @@ export class ApiService implements ApiServiceAbstraction { ); } - protected async refreshToken(): Promise { + // Keep the running refreshTokenPromise to prevent parallel calls. + protected refreshToken(): Promise { + if (this.refreshTokenPromise === undefined) { + this.refreshTokenPromise = this.internalRefreshToken(); + void this.refreshTokenPromise.finally(() => { + this.refreshTokenPromise = undefined; + }); + } + return this.refreshTokenPromise; + } + + private async internalRefreshToken(): Promise { const refreshToken = await this.tokenService.getRefreshToken(); if (refreshToken != null && refreshToken !== "") { return this.refreshAccessToken(); @@ -1835,7 +1858,7 @@ export class ApiService implements ApiServiceAbstraction { } async send( - method: "GET" | "POST" | "PUT" | "DELETE", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, authed: boolean, @@ -1875,7 +1898,7 @@ export class ApiService implements ApiServiceAbstraction { return responseJson; } else if (hasResponse && response.status === 200 && responseIsCsv) { return await response.text(); - } else if (response.status !== 200) { + } else if (response.status !== 200 && response.status !== 204) { const error = await this.handleError(response, false, authed); return Promise.reject(error); } diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index b06985e0ba7..da38ca5bfff 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, from, zip, Observable } from "rxjs"; +import { firstValueFrom, map, from, zip } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; @@ -17,8 +20,6 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { EVENT_COLLECTION } from "./key-definitions"; export class EventCollectionService implements EventCollectionServiceAbstraction { - private orgIds$: Observable; - constructor( private cipherService: CipherService, private stateProvider: StateProvider, @@ -26,11 +27,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private eventUploadService: EventUploadService, private authService: AuthService, private accountService: AccountService, - ) { - this.orgIds$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), - ); - } + ) {} + + private getOrgIds = (orgs: Organization[]): string[] => { + return orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []; + }; /** Adds an event to the active user's event collection * @param eventType the event type to be added @@ -42,14 +43,15 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ciphers: CipherView[], uploadImmediately = false, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(null, eventType, ciphers))) { return; } - const events$ = this.orgIds$.pipe( + const events$ = this.organizationService.organizations$(userId).pipe( + map((orgs) => this.getOrgIds(orgs)), map((orgs) => ciphers .filter((c) => orgs.includes(c.organizationId)) @@ -86,7 +88,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction uploadImmediately = false, organizationId: string = null, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) { @@ -122,8 +124,14 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ): Promise { const cipher$ = from(this.cipherService.get(cipherId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const orgIds$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => this.getOrgIds(orgs))); + const [authStatus, orgIds, cipher] = await firstValueFrom( - zip(this.authService.activeAccountStatus$, this.orgIds$, cipher$), + zip(this.authService.activeAccountStatus$, orgIds$, cipher$), ); // The user must be authorized diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index 4a14332af8a..f88c904bee1 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -227,6 +227,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.syncService.fullSync(true); } break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + if (isAuthenticated) { + await this.syncService.fullSync(true); + } + break; default: break; } diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 540f26bba2d..77ed6c960ab 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -6,10 +6,10 @@ import { FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,10 +18,12 @@ import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { LogService } from "../../platform/abstractions/log.service"; +import { Utils } from "../../platform/misc/utils"; import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION, } from "../../services/vault-timeout/vault-timeout-settings.state"; +import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index 0f5bdca93da..ffc8b6e0144 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -21,6 +21,8 @@ import { } from "@bitwarden/auth/common"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 8a166e63a1f..986bbbf95a4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -3,8 +3,6 @@ import { BehaviorSubject, from, of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { BiometricsService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; @@ -15,10 +13,12 @@ import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; +import { TaskSchedulerService } from "../../platform/scheduling"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 8ab10b44b24..08dc02bb1ab 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -4,8 +4,6 @@ import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from " import { CollectionService } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { BiometricsService } from "@bitwarden/key-management"; import { SearchService } from "../../abstractions/search.service"; @@ -16,9 +14,11 @@ import { AuthService } from "../../auth/abstractions/auth.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "../../platform/scheduling"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 81b1016a53d..169de447f10 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -66,13 +66,14 @@ import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-sett import { MoveFinalDesktopSettingsMigrator } from "./migrations/66-move-final-desktop-settings"; import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-unassigned-items-banner-dismissed"; import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; +import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 68; +export const CURRENT_VERSION = 69; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -142,7 +143,8 @@ export function createMigrationBuilder() { .with(ForwarderOptionsMigrator, 64, 65) .with(MoveFinalDesktopSettingsMigrator, 65, 66) .with(RemoveUnassignedItemsBannerDismissed, 66, 67) - .with(MoveLastSyncDate, 67, CURRENT_VERSION); + .with(MoveLastSyncDate, 67, 68) + .with(MigrateIncorrectFolderKey, 68, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts index 53ae94c30b2..84bcaeec608 100644 --- a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts @@ -3,6 +3,8 @@ import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const AutofillOverlayVisibility = { Off: 0, OnButtonClick: 1, diff --git a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts index 04b4a50e0ed..9397191f54b 100644 --- a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts @@ -3,6 +3,8 @@ import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const ClearClipboardDelay = { Never: null as null, TenSeconds: 10, diff --git a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts index b465a69131a..71e5c531d50 100644 --- a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts @@ -3,6 +3,8 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const UriMatchStrategy = { Domain: 0, Host: 1, diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts new file mode 100644 index 00000000000..e5dec943f78 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts @@ -0,0 +1,98 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MigrateIncorrectFolderKey } from "./69-migrate-incorrect-folder-key"; + +function exampleJSON() { + return { + global_account_accounts: { + user1: null as any, + user2: null as any, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + user_user2_folder_folder: null as any, + }; +} + +describe("MigrateIncorrectFolderKey", () => { + const sut = new MigrateIncorrectFolderKey(68, 69); + it("migrates data", async () => { + const output = await runMigrator(sut, exampleJSON()); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + // Correct "folders" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); + + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts new file mode 100644 index 00000000000..046c0cf0dfa --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts @@ -0,0 +1,45 @@ +import { + KeyDefinitionLike, + MigrationHelper, +} from "@bitwarden/common/state-migrations/migration-helper"; +import { Migrator } from "@bitwarden/common/state-migrations/migrator"; + +const BAD_FOLDER_KEY: KeyDefinitionLike = { + key: "folder", // We inadvertently changed the key from "folders" to "folder" + stateDefinition: { + name: "folder", + }, +}; + +const GOOD_FOLDER_KEY: KeyDefinitionLike = { + key: "folders", // We should keep the key as "folders" + stateDefinition: { + name: "folder", + }, +}; + +export class MigrateIncorrectFolderKey extends Migrator<68, 69> { + async migrate(helper: MigrationHelper): Promise { + async function migrateUser(userId: string) { + const value = await helper.getFromUser(userId, BAD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, GOOD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, BAD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => migrateUser(userId))); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string) { + const value = await helper.getFromUser(userId, GOOD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, BAD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, GOOD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => rollbackUser(userId))); + } +} diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 12257905d1c..0b60aef4917 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -1,13 +1,13 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, Subject } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../types/csprng"; +import { OrganizationId, UserId } from "../../types/guid"; +import { OrgKey, UserKey } from "../../types/key"; import { OrganizationBound, UserBound } from "../dependencies"; import { KeyServiceLegacyEncryptorProvider } from "./key-service-legacy-encryptor-provider"; @@ -184,7 +184,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { singleUserId$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes when `userKey$` emits a falsy value after emitting a truthy value", () => { @@ -199,7 +199,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { userKey$.next(null); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes once `dependencies.singleUserId$` emits and `userKey$` completes", () => { @@ -214,7 +214,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { userKey$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); }); @@ -445,7 +445,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { singleOrganizationId$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes when `orgKeys$` emits a falsy value after emitting a truthy value", () => { @@ -466,7 +466,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { orgKey$.next(OrgRecords); orgKey$.next(null); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes once `dependencies.singleOrganizationId$` emits and `userKey$` completes", () => { @@ -486,7 +486,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { orgKey$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); }); }); diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts index 3eee08eb6bd..d4a8dec7dc3 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts @@ -12,10 +12,10 @@ import { takeWhile, } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { OrganizationId, UserId } from "../../types/guid"; import { OrganizationBound, SingleOrganizationDependency, diff --git a/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts b/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts index dda8bfe957a..9eec207e92d 100644 --- a/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts +++ b/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; - import { EncString } from "../../platform/models/domain/enc-string"; +import { OrganizationId } from "../../types/guid"; /** An encryption strategy that protects a type's secrets with * organization-specific keys. This strategy is bound to a specific organization. diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.ts index 3fdc0a1da32..d3b7dae10f5 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; - import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { OrganizationId } from "../../types/guid"; import { OrgKey } from "../../types/key"; import { DataPacker } from "../state/data-packer.abstraction"; diff --git a/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts b/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts index d63a5e908ef..6bb0e252af3 100644 --- a/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts +++ b/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; /** An encryption strategy that protects a type's secrets with * user-specific keys. This strategy is bound to a specific user. diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.ts b/libs/common/src/tools/cryptography/user-key-encryptor.ts index 15a3a1b6d5c..296c33ea1dc 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; import { DataPacker } from "../state/data-packer.abstraction"; diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index cdae45bc94a..befe1ca5406 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -152,7 +152,8 @@ export type SingleUserDependency = { }; /** A pattern for types that emit values exclusively when the dependency - * emits a message. + * emits a message. Set a type parameter when your method requires contextual + * information when the request is issued. * * Consumers of this dependency should emit when `on$` emits. If `on$` * completes, the consumer should also complete. If `on$` @@ -161,10 +162,10 @@ export type SingleUserDependency = { * @remarks This dependency is useful when you have a nondeterministic * or stateful algorithm that you would like to run when an event occurs. */ -export type OnDependency = { +export type OnDependency = { /** The stream that controls emissions */ - on$: Observable; + on$: Observable; }; /** A pattern for types that emit when a dependency is `true`. diff --git a/libs/common/src/tools/extension/data.ts b/libs/common/src/tools/extension/data.ts new file mode 100644 index 00000000000..cab6272a068 --- /dev/null +++ b/libs/common/src/tools/extension/data.ts @@ -0,0 +1,26 @@ +/** well-known name for a feature extensible through an extension. */ +export const Site = Object.freeze({ + forwarder: "forwarder", +} as const); + +/** well-known name for a field surfaced from an extension site to a vendor. */ +export const Field = Object.freeze({ + token: "token", + baseUrl: "baseUrl", + domain: "domain", + prefix: "prefix", +} as const); + +/** Permission levels for metadata. */ +export const Permission = Object.freeze({ + /** unless a rule denies access, allow it. If a permission is `null` + * or `undefined` it should be treated as `Permission.default`. + */ + default: "default", + /** unless a rule allows access, deny it. */ + none: "none", + /** access is explicitly granted to use an extension. */ + allow: "allow", + /** access is explicitly prohibited for this extension. This rule overrides allow rules. */ + deny: "deny", +} as const); diff --git a/libs/common/src/tools/extension/extension-registry.abstraction.ts b/libs/common/src/tools/extension/extension-registry.abstraction.ts new file mode 100644 index 00000000000..7734c01ea50 --- /dev/null +++ b/libs/common/src/tools/extension/extension-registry.abstraction.ts @@ -0,0 +1,104 @@ +import { ExtensionSite } from "./extension-site"; +import { + ExtensionMetadata, + ExtensionSet, + ExtensionPermission, + SiteId, + SiteMetadata, + VendorId, + VendorMetadata, +} from "./type"; + +/** Tracks extension sites and the vendors that extend them. */ +export abstract class ExtensionRegistry { + /** Registers a site supporting extensibility. + * Each site may only be registered once. Calls after the first for + * the same SiteId have no effect. + * @param site identifies the site being extended + * @param meta configures the extension site + * @return self for method chaining. + * @remarks The registry initializes with a set of allowed sites and fields. + * `registerSite` drops a registration and trims its allowed fields to only + * those indicated in the allow list. + */ + abstract registerSite: (meta: SiteMetadata) => this; + + /** List all registered extension sites with their extension permission, if any. + * @returns a list of all extension sites. `permission` is defined when the site + * is associated with an extension permission. + */ + abstract sites: () => { site: SiteMetadata; permission?: ExtensionPermission }[]; + + /** Get a site's metadata + * @param site identifies a site registration + * @return the site's metadata or `undefined` if the site isn't registered. + */ + abstract site: (site: SiteId) => SiteMetadata | undefined; + + /** Registers a vendor providing an extension. + * Each vendor may only be registered once. Calls after the first for + * the same VendorId have no effect. + * @param site - identifies the site being extended + * @param meta - configures the extension site + * @return self for method chaining. + */ + abstract registerVendor: (meta: VendorMetadata) => this; + + /** List all registered vendors with their permissions, if any. + * @returns a list of all extension sites. `permission` is defined when the site + * is associated with an extension permission. + */ + abstract vendors: () => { vendor: VendorMetadata; permission?: ExtensionPermission }[]; + + /** Get a vendor's metadata + * @param site identifies a vendor registration + * @return the vendor's metadata or `undefined` if the vendor isn't registered. + */ + abstract vendor: (vendor: VendorId) => VendorMetadata | undefined; + + /** Registers an extension provided by a vendor to an extension site. + * The vendor and site MUST be registered before the extension. + * Each extension may only be registered once. Calls after the first for + * the same SiteId and VendorId have no effect. + * @param site - identifies the site being extended + * @param meta - configures the extension site + * @return self for method chaining. + */ + abstract registerExtension: (meta: ExtensionMetadata) => this; + + /** Get an extensions metadata + * @param site identifies the extension's site + * @param vendor identifies the extension's vendor + * @return the extension's metadata or `undefined` if the extension isn't registered. + */ + abstract extension: (site: SiteId, vendor: VendorId) => ExtensionMetadata | undefined; + + /** List all registered extensions and their permissions */ + abstract extensions: () => ReadonlyArray<{ + extension: ExtensionMetadata; + permissions: ExtensionPermission[]; + }>; + + /** Registers a permission. Only 1 permission can be registered for each extension set. + * Calls after the first *replace* the registered permission. + * @param set the collection of extensions affected by the permission + * @param permission the permission for the collection + * @return self for method chaining. + */ + abstract setPermission: (set: ExtensionSet, permission: ExtensionPermission) => this; + + /** Retrieves the current permission for the given extension set or `undefined` if + * a permission doesn't exist. + */ + abstract permission: (set: ExtensionSet) => ExtensionPermission | undefined; + + /** Returns all registered extension rules. */ + abstract permissions: () => { set: ExtensionSet; permission: ExtensionPermission }[]; + + /** Creates a point-in-time snapshot of the registry's contents with extension + * permissions applied for the provided SiteId. + * @param id identifies the extension site to create. + * @returns the extension site, or `undefined` if the site is not registered. + */ + abstract build: (id: SiteId) => ExtensionSite | undefined; +} diff --git a/libs/common/src/tools/extension/extension-site.ts b/libs/common/src/tools/extension/extension-site.ts new file mode 100644 index 00000000000..e8aba008493 --- /dev/null +++ b/libs/common/src/tools/extension/extension-site.ts @@ -0,0 +1,20 @@ +import { deepFreeze } from "../util"; + +import { ExtensionMetadata, SiteMetadata, VendorId } from "./type"; + +/** Describes the capabilities of an extension site. + * This type is immutable. + */ +export class ExtensionSite { + /** instantiate the extension site + * @param site describes the extension site + * @param vendors describes the available vendors + * @param extensions describes the available extensions + */ + constructor( + readonly site: Readonly, + readonly extensions: ReadonlyMap>, + ) { + deepFreeze(this); + } +} diff --git a/libs/common/src/tools/extension/factory.ts b/libs/common/src/tools/extension/factory.ts new file mode 100644 index 00000000000..10ebc77804a --- /dev/null +++ b/libs/common/src/tools/extension/factory.ts @@ -0,0 +1,24 @@ +import { DefaultFields, DefaultSites, Extension } from "./metadata"; +import { RuntimeExtensionRegistry } from "./runtime-extension-registry"; +import { VendorExtensions, Vendors } from "./vendor"; + +// FIXME: find a better way to build the registry than a hard-coded factory function + +/** Constructs the extension registry */ +export function buildExtensionRegistry() { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + for (const site of Reflect.ownKeys(Extension) as string[]) { + registry.registerSite(Extension[site]); + } + + for (const vendor of Vendors) { + registry.registerVendor(vendor); + } + + for (const extension of VendorExtensions) { + registry.registerExtension(extension); + } + + return registry; +} diff --git a/libs/common/src/tools/extension/index.ts b/libs/common/src/tools/extension/index.ts new file mode 100644 index 00000000000..e786dde4f59 --- /dev/null +++ b/libs/common/src/tools/extension/index.ts @@ -0,0 +1,12 @@ +export { Site, Field, Permission } from "./data"; +export { + SiteId, + FieldId, + VendorId, + ExtensionId, + ExtensionPermission, + SiteMetadata, + ExtensionMetadata, + VendorMetadata, +} from "./type"; +export { ExtensionSite } from "./extension-site"; diff --git a/libs/common/src/tools/extension/metadata.ts b/libs/common/src/tools/extension/metadata.ts new file mode 100644 index 00000000000..895b1d1b31f --- /dev/null +++ b/libs/common/src/tools/extension/metadata.ts @@ -0,0 +1,17 @@ +import { Field, Site, Permission } from "./data"; +import { FieldId, SiteId, SiteMetadata } from "./type"; + +export const DefaultSites: SiteId[] = Object.freeze(Object.keys(Site) as any); + +export const DefaultFields: FieldId[] = Object.freeze(Object.keys(Field) as any); + +export const Extension: Record = { + [Site.forwarder]: { + id: Site.forwarder, + availableFields: [Field.baseUrl, Field.domain, Field.prefix, Field.token], + }, +}; + +export const AllowedPermissions: ReadonlyArray = Object.freeze( + Object.values(Permission), +); diff --git a/libs/common/src/tools/extension/runtime-extension-registry.spec.ts b/libs/common/src/tools/extension/runtime-extension-registry.spec.ts new file mode 100644 index 00000000000..6aa7382db57 --- /dev/null +++ b/libs/common/src/tools/extension/runtime-extension-registry.spec.ts @@ -0,0 +1,923 @@ +import { deepFreeze } from "../util"; + +import { Field, Site, Permission } from "./data"; +import { ExtensionSite } from "./extension-site"; +import { DefaultFields, DefaultSites } from "./metadata"; +import { RuntimeExtensionRegistry } from "./runtime-extension-registry"; +import { ExtensionMetadata, SiteId, SiteMetadata, VendorMetadata } from "./type"; +import { Bitwarden } from "./vendor/bitwarden"; + +// arbitrary test entities +const SomeSiteId: SiteId = Site.forwarder; + +const SomeSite: SiteMetadata = Object.freeze({ + id: SomeSiteId, + availableFields: [], +}); + +const SomeVendor = Bitwarden; +const SomeVendorId = SomeVendor.id; +const SomeExtension: ExtensionMetadata = deepFreeze({ + site: SomeSite, + product: { vendor: SomeVendor, name: "Some Product" }, + host: { authorization: "bearer", selfHost: "maybe", baseUrl: "https://vault.bitwarden.com" }, + requestedFields: [], +}); + +const JustTrustUs: VendorMetadata = Object.freeze({ + id: "justrustus" as any, + name: "JustTrust.Us", +}); +const JustTrustUsExtension: ExtensionMetadata = deepFreeze({ + site: SomeSite, + product: { vendor: JustTrustUs }, + host: { authorization: "bearer", selfHost: "maybe", baseUrl: "https://justrust.us" }, + requestedFields: [], +}); + +// In the following tests, not-null assertions (`!`) indicate that +// the returned object should never be null or undefined given +// the conditions defined within the test case +describe("RuntimeExtensionRegistry", () => { + describe("registerSite", () => { + it("registers an extension site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + + const result = registry.registerSite(SomeSite).site(SomeSiteId); + + expect(result).toEqual(SomeSite); + }); + + it("interns the site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + + const result = registry.registerSite(SomeSite).site(SomeSiteId); + + expect(result).not.toBe(SomeSite); + }); + + it("registers an extension site with fields", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + const site: SiteMetadata = { + ...SomeSite, + availableFields: [Field.baseUrl], + }; + + const result = registry.registerSite(site).site(SomeSiteId); + + expect(result).toEqual(site); + }); + + it("ignores unavailable sites", () => { + const registry = new RuntimeExtensionRegistry([], []); + const ignored: SiteMetadata = { + id: "an-unavailable-site" as any, + availableFields: [], + }; + + const result = registry.registerSite(ignored).sites(); + + expect(result).toEqual([]); + }); + + it("ignores duplicate registrations", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const ignored: SiteMetadata = { + ...SomeSite, + availableFields: [Field.token], + }; + + const result = registry.registerSite(SomeSite).registerSite(ignored).site(SomeSiteId); + + expect(result).toEqual(SomeSite); + }); + + it("ignores unknown available fields", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + const ignored: SiteMetadata = { + ...SomeSite, + availableFields: [SomeSite.availableFields, "ignored" as any], + }; + + const { availableFields } = registry.registerSite(ignored).site(SomeSiteId)!; + + expect(availableFields).toEqual(SomeSite.availableFields); + }); + + it("freezes the site definition", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const site = registry.registerSite(SomeSite).site(SomeSiteId)!; + + // reassigning `availableFields` throws b/c the object is frozen + expect(() => (site.availableFields = [Field.domain])).toThrow(); + }); + }); + + describe("site", () => { + it("returns `undefined` for an unknown site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry.site(SomeSiteId); + + expect(result).toBeUndefined(); + }); + + it("returns the same result when called repeatedly", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerSite(SomeSite); + + const first = registry.site(SomeSiteId); + const second = registry.site(SomeSiteId); + + expect(first).toBe(second); + }); + }); + + describe("sites", () => { + it("lists registered sites", () => { + const registry = new RuntimeExtensionRegistry([SomeSiteId, "bar"] as any[], DefaultFields); + const barSite: SiteMetadata = { + id: "bar" as any, + availableFields: [], + }; + + const result = registry.registerSite(SomeSite).registerSite(barSite).sites(); + + expect(result.some(({ site }) => site.id === SomeSiteId)).toBe(true); + expect(result.some(({ site }) => site.id === barSite.id)).toBe(true); + }); + + it("includes permissions for a site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry + .registerSite(SomeSite) + .setPermission({ site: SomeSite.id }, Permission.allow) + .sites(); + + expect(result).toEqual([{ site: SomeSite, permission: Permission.allow }]); + }); + + it("ignores duplicate registrations", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + const ignored: SiteMetadata = { + ...SomeSite, + availableFields: [Field.token], + }; + + const result = registry.registerSite(SomeSite).registerSite(ignored).sites(); + + expect(result).toEqual([{ site: SomeSite }]); + }); + + it("ignores permissions for other sites", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry + .registerSite(SomeSite) + .setPermission({ site: SomeSite.id }, Permission.allow) + .setPermission({ site: "bar" as any }, Permission.deny) + .sites(); + + expect(result).toEqual([{ site: SomeSite, permission: Permission.allow }]); + }); + }); + + describe("registerVendor", () => { + it("registers a vendor", () => { + const registry = new RuntimeExtensionRegistry([], []); + const result = registry.registerVendor(SomeVendor).vendors(); + + expect(result).toEqual([{ vendor: SomeVendor }]); + }); + + it("freezes the vendor definition", () => { + const registry = new RuntimeExtensionRegistry([], []); + // copy `SomeVendor` because it is already frozen + const original: VendorMetadata = { ...SomeVendor }; + + const [{ vendor }] = registry.registerVendor(original).vendors(); + + // reassigning `name` throws b/c the object is frozen + expect(() => (vendor.name = "Bytewarden")).toThrow(); + }); + }); + + describe("vendor", () => { + it("returns `undefined` for an unknown site", () => { + const registry = new RuntimeExtensionRegistry([], []); + + const result = registry.vendor(SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("returns the same result when called repeatedly", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerVendor(SomeVendor); + + const first = registry.vendor(SomeVendorId); + const second = registry.vendor(SomeVendorId); + + expect(first).toBe(second); + }); + }); + + describe("vendors", () => { + it("lists registered vendors", () => { + const registry = new RuntimeExtensionRegistry([], []); + registry.registerVendor(SomeVendor).registerVendor(JustTrustUs); + + const result = registry.vendors(); + + expect(result.some(({ vendor }) => vendor.id === SomeVendorId)).toBe(true); + expect(result.some(({ vendor }) => vendor.id === JustTrustUs.id)).toBe(true); + }); + + it("includes permissions for a vendor", () => { + const registry = new RuntimeExtensionRegistry([], []); + + const result = registry + .registerVendor(SomeVendor) + .setPermission({ vendor: SomeVendorId }, Permission.allow) + .vendors(); + + expect(result).toEqual([{ vendor: SomeVendor, permission: Permission.allow }]); + }); + + it("ignores duplicate registrations", () => { + const registry = new RuntimeExtensionRegistry([], []); + const vendor: VendorMetadata = SomeVendor; + const ignored: VendorMetadata = { + ...SomeVendor, + name: "Duplicate", + }; + + const result = registry.registerVendor(vendor).registerVendor(ignored).vendors(); + + expect(result).toEqual([{ vendor }]); + }); + + it("ignores permissions for other sites", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerVendor(SomeVendor).setPermission({ vendor: SomeVendorId }, Permission.allow); + + const result = registry.setPermission({ vendor: JustTrustUs.id }, Permission.deny).vendors(); + + expect(result).toEqual([{ vendor: SomeVendor, permission: Permission.allow }]); + }); + }); + + describe("setPermission", () => { + it("sets the all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { all: true } as const; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("sets a vendor permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { vendor: SomeVendorId }; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("sets a site permission", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const target = { site: SomeSiteId }; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("ignores a site permission unless it is in the allowed sites list", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { site: SomeSiteId }; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toBeUndefined(); + }); + + it("throws when a permission is invalid", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + + expect(() => registry.setPermission({ all: true }, "invalid" as any)).toThrow(); + }); + + it("throws when the extension set is the wrong type", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { invalid: "invalid" } as any; + + expect(() => registry.setPermission(target, Permission.allow)).toThrow(); + }); + }); + + describe("permission", () => { + it("gets the default all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { all: true } as const; + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.default); + }); + + it("gets an all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { all: true } as const; + registry.setPermission(target, Permission.none); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.none); + }); + + it("gets a vendor permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { vendor: SomeVendorId }; + registry.setPermission(target, Permission.allow); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("gets a site permission", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const target = { site: SomeSiteId }; + registry.setPermission(target, Permission.allow); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("gets a vendor permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { vendor: SomeVendorId }; + registry.setPermission(target, Permission.allow); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("returns undefined when the extension set is the wrong type", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { invalid: "invalid" } as any; + + const permission = registry.permission(target); + + expect(permission).toBeUndefined(); + }); + }); + + describe("permissions", () => { + it("returns a default all permission by default", () => { + const registry = new RuntimeExtensionRegistry([], []); + + const permission = registry.permissions(); + + expect(permission).toEqual([{ set: { all: true }, permission: Permission.default }]); + }); + + it("returns the all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + registry.setPermission({ all: true }, Permission.none); + + const permission = registry.permissions(); + + expect(permission).toEqual([{ set: { all: true }, permission: Permission.none }]); + }); + + it("includes site permissions", () => { + const registry = new RuntimeExtensionRegistry([SomeSiteId, "bar"] as any[], DefaultFields); + registry.registerSite(SomeSite).setPermission({ site: SomeSiteId }, Permission.allow); + registry + .registerSite({ + id: "bar" as any, + availableFields: [], + }) + .setPermission({ site: "bar" as any }, Permission.deny); + + const result = registry.permissions(); + + expect( + result.some((p: any) => p.set.site === SomeSiteId && p.permission === Permission.allow), + ).toBe(true); + expect( + result.some((p: any) => p.set.site === "bar" && p.permission === Permission.deny), + ).toBe(true); + }); + + it("includes vendor permissions", () => { + const registry = new RuntimeExtensionRegistry([], DefaultFields); + registry.registerVendor(SomeVendor).setPermission({ vendor: SomeVendorId }, Permission.allow); + registry + .registerVendor(JustTrustUs) + .setPermission({ vendor: JustTrustUs.id }, Permission.deny); + + const result = registry.permissions(); + + expect( + result.some((p: any) => p.set.vendor === SomeVendorId && p.permission === Permission.allow), + ).toBe(true); + expect( + result.some( + (p: any) => p.set.vendor === JustTrustUs.id && p.permission === Permission.deny, + ), + ).toBe(true); + }); + }); + + describe("registerExtension", () => { + it("registers an extension", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId); + + expect(result).toEqual(SomeExtension); + }); + + it("ignores extensions with nonregistered sites", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerVendor(SomeVendor); + + // precondition: the site is not registered + expect(registry.site(SomeSiteId)).toBeUndefined(); + + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("ignores extensions with nonregistered vendors", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite); + + // precondition: the vendor is not registered + expect(registry.vendor(SomeVendorId)).toBeUndefined(); + + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("ignores repeated extensions with nonregistered vendors", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + + // precondition: the vendor is already registered + expect(registry.extension(SomeSiteId, SomeVendorId)).toBeDefined(); + + const result = registry + .registerExtension({ + ...SomeExtension, + requestedFields: [Field.domain], + }) + .extension(SomeSiteId, SomeVendorId); + + expect(result).toEqual(SomeExtension); + }); + + it("interns site metadata", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const internedSite = registry.site(SomeSiteId); + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId)!; + + expect(result.site).toBe(internedSite); + }); + + it("interns vendor metadata", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const internedVendor = registry.vendor(SomeVendorId); + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId)!; + + expect(result.product.vendor).toBe(internedVendor); + }); + + it("freezes the extension metadata", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + const extension = registry.extension(SomeSiteId, SomeVendorId)!; + + // field assignments & mutation functions throw b/c the object is frozen + expect(() => ((extension.site as any) = SomeSite)).toThrow(); + expect(() => ((extension.product.vendor as any) = SomeVendor)).toThrow(); + expect(() => ((extension.product.name as any) = "SomeVendor")).toThrow(); + expect(() => ((extension.host as any) = {})).toThrow(); + expect(() => ((extension.host.selfHost as any) = {})).toThrow(); + expect(() => ((extension.host as any).authorization = "basic")).toThrow(); + expect(() => ((extension.host as any).baseUrl = "https://www.example.com")).toThrow(); + expect(() => ((extension.requestedFields as any) = [Field.baseUrl])).toThrow(); + expect(() => (extension.requestedFields as any).push(Field.baseUrl)).toThrow(); + }); + }); + + describe("extension", () => { + describe("extension", () => { + it("returns `undefined` for an unknown extension", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry.extension(SomeSiteId, SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("interns the extension", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + + const first = registry.extension(SomeSiteId, SomeVendorId); + const second = registry.extension(SomeSiteId, SomeVendorId); + + expect(first).toBe(second); + }); + }); + + describe("extensions", () => { + it("lists registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerSite(SomeSite); + registry.registerVendor(SomeVendor).registerExtension(SomeExtension); + registry.registerVendor(JustTrustUs).registerExtension(JustTrustUsExtension); + + const result = registry.extensions(); + + expect( + result.some( + ({ extension }) => + extension.site.id === SomeSiteId && extension.product.vendor.id === SomeVendorId, + ), + ).toBe(true); + expect( + result.some( + ({ extension }) => + extension.site.id === SomeSiteId && extension.product.vendor.id === JustTrustUs.id, + ), + ).toBe(true); + }); + + it("includes permissions for extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ vendor: SomeVendorId }, Permission.allow); + + const result = registry.extensions(); + + expect( + result.some( + ({ extension, permissions }) => + extension.site.id === SomeSiteId && + extension.product.vendor.id === SomeVendorId && + permissions.includes(Permission.allow), + ), + ).toBe(true); + }); + }); + + describe("build", () => { + it("builds an empty extension site when no extensions are registered", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }); + + it("builds an extension site with all registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + const expected = registry.extension(SomeSiteId, SomeVendorId); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.get(SomeVendorId)).toBe(expected); + }); + + it("returns `undefined` for an unknown site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry.build(SomeSiteId); + + expect(result).toBeUndefined(); + }); + + describe("when the all permission is `default`", () => { + const allPermission = Permission.default; + + it("builds an extension site with all registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.default); + const expected = registry.extension(SomeSiteId, SomeVendorId); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toBe(expected); + }); + + it.each([[Permission.default], [Permission.allow]])( + "includes sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.none], [Permission.deny]])( + "ignores sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + + it.each([[Permission.default], [Permission.allow]])( + "includes vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.none], [Permission.deny]])( + "ignores vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + }); + + describe("when the all permission is `none`", () => { + const allPermission = Permission.none; + + it("builds an empty extension site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.none); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.size).toBe(0); + }); + + it.each([[Permission.allow]])("includes sites with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }); + + it.each([[Permission.default], [Permission.none], [Permission.deny]])( + "ignores sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + + it.each([[Permission.allow]])("includes vendors with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }); + + it.each([[Permission.default], [Permission.none], [Permission.deny]])( + "ignores vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + }); + + describe("when the all permission is `allow`", () => { + const allPermission = Permission.allow; + + it("builds an extension site with all registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.default); + const expected = registry.extension(SomeSiteId, SomeVendorId); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.get(SomeVendorId)).toBe(expected); + }); + + it.each([[Permission.default], [Permission.none], [Permission.allow]])( + "includes sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.deny]])("ignores sites with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }); + + it.each([[Permission.default], [Permission.none], [Permission.allow]])( + "includes vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.deny]])("ignores vendors with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }); + }); + + describe("when the all permission is `deny`", () => { + const allPermission = Permission.deny; + + it("builds an empty extension site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.deny); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.size).toBe(0); + }); + + it.each([[Permission.default], [Permission.none], [Permission.allow], [Permission.deny]])( + "ignores sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + + it.each([[Permission.default], [Permission.none], [Permission.allow], [Permission.deny]])( + "ignores vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + }); + }); + }); +}); diff --git a/libs/common/src/tools/extension/runtime-extension-registry.ts b/libs/common/src/tools/extension/runtime-extension-registry.ts new file mode 100644 index 00000000000..1c630dcc915 --- /dev/null +++ b/libs/common/src/tools/extension/runtime-extension-registry.ts @@ -0,0 +1,286 @@ +import { deepFreeze } from "../util"; + +import { ExtensionRegistry } from "./extension-registry.abstraction"; +import { ExtensionSite } from "./extension-site"; +import { AllowedPermissions } from "./metadata"; +import { + ExtensionMetadata, + ExtensionPermission, + ExtensionSet, + FieldId, + ProductMetadata, + SiteMetadata, + SiteId, + VendorId, + VendorMetadata, +} from "./type"; + +/** Tracks extension sites and the vendors that extend them in application memory. */ +export class RuntimeExtensionRegistry implements ExtensionRegistry { + /** Instantiates the extension registry + * @param allowedSites sites that are valid for use by any extension; + * this is most useful to disable an extension site that is only + * available on a specific client. + * @param allowedFields fields that are valid for use by any extension; + * this is most useful to prohibit access to a field via policy. + */ + constructor( + private readonly allowedSites: SiteId[], + private readonly allowedFields: FieldId[], + ) { + Object.freeze(this.allowedFields); + Object.freeze(this.allowedSites); + } + + private allPermission: ExtensionPermission = "default"; + + private siteRegistrations = new Map(); + private sitePermissions = new Map(); + + private vendorRegistrations = new Map(); + private vendorPermissions = new Map(); + + private extensionRegistrations = new Array(); + private extensionsBySiteByVendor = new Map>(); + + registerSite(site: SiteMetadata): this { + if (!this.allowedSites.includes(site.id)) { + return this; + } + + // verify requested fields are on the list of valid fields to expose to + // an extension + const availableFields = site.availableFields.filter((field) => + this.allowedFields.includes(field), + ); + const validated: SiteMetadata = deepFreeze({ id: site.id, availableFields }); + + if (!this.siteRegistrations.has(site.id)) { + this.siteRegistrations.set(site.id, validated); + } + + return this; + } + + site(site: SiteId): SiteMetadata | undefined { + const result = this.siteRegistrations.get(site); + return result; + } + + sites() { + const sites: { site: SiteMetadata; permission?: ExtensionPermission }[] = []; + + for (const [k, site] of this.siteRegistrations.entries()) { + const s: (typeof sites)[number] = { site }; + const permission = this.sitePermissions.get(k); + if (permission) { + s.permission = permission; + } + + sites.push(s); + } + + return sites; + } + + registerVendor(vendor: VendorMetadata): this { + if (!this.vendorRegistrations.has(vendor.id)) { + const frozen = deepFreeze(vendor); + this.vendorRegistrations.set(vendor.id, frozen); + } + + return this; + } + + vendor(vendor: VendorId): VendorMetadata | undefined { + const result = this.vendorRegistrations.get(vendor); + return result; + } + + vendors() { + const vendors: { vendor: VendorMetadata; permission?: ExtensionPermission }[] = []; + + for (const [k, vendor] of this.vendorRegistrations.entries()) { + const s: (typeof vendors)[number] = { vendor }; + const permission = this.vendorPermissions.get(k); + if (permission) { + s.permission = permission; + } + + vendors.push(s); + } + + return vendors; + } + + setPermission(set: ExtensionSet, permission: ExtensionPermission): this { + if (!AllowedPermissions.includes(permission)) { + throw new Error(`invalid extension permission: ${permission}`); + } + + if ("all" in set && set.all) { + this.allPermission = permission; + } else if ("vendor" in set) { + this.vendorPermissions.set(set.vendor, permission); + } else if ("site" in set) { + if (this.allowedSites.includes(set.site)) { + this.sitePermissions.set(set.site, permission); + } + } else { + throw new Error(`Unrecognized extension set received: ${JSON.stringify(set)}.`); + } + + return this; + } + + permission(set: ExtensionSet) { + if ("all" in set && set.all) { + return this.allPermission; + } else if ("vendor" in set) { + return this.vendorPermissions.get(set.vendor); + } else if ("site" in set) { + return this.sitePermissions.get(set.site); + } else { + return undefined; + } + } + + permissions() { + const rules: { set: ExtensionSet; permission: ExtensionPermission }[] = []; + rules.push({ set: { all: true }, permission: this.allPermission }); + + for (const [site, permission] of this.sitePermissions.entries()) { + rules.push({ set: { site }, permission }); + } + + for (const [vendor, permission] of this.vendorPermissions.entries()) { + rules.push({ set: { vendor }, permission }); + } + + return rules; + } + + registerExtension(meta: ExtensionMetadata): this { + const site = this.siteRegistrations.get(meta.site.id); + const vendor = this.vendorRegistrations.get(meta.product.vendor.id); + if (!site || !vendor) { + return this; + } + + // exit early if the extension is already registered + const extensionsByVendor = + this.extensionsBySiteByVendor.get(meta.site.id) ?? new Map(); + if (extensionsByVendor.has(meta.product.vendor.id)) { + return this; + } + + // create immutable copy; this updates the vendor and site with + // their internalized representation to provide reference equality + // across registrations + const product: ProductMetadata = { vendor }; + if (meta.product.name) { + product.name = meta.product.name; + } + const extension: ExtensionMetadata = Object.freeze({ + site, + product: Object.freeze(product), + host: Object.freeze({ ...meta.host }), + requestedFields: Object.freeze([...meta.requestedFields]), + }); + + // register it + const index = this.extensionRegistrations.push(extension) - 1; + extensionsByVendor.set(vendor.id, index); + this.extensionsBySiteByVendor.set(site.id, extensionsByVendor); + + return this; + } + + extension(site: SiteId, vendor: VendorId): ExtensionMetadata | undefined { + const index = this.extensionsBySiteByVendor.get(site)?.get(vendor) ?? -1; + if (index < 0) { + return undefined; + } else { + return this.extensionRegistrations[index]; + } + } + + private getPermissions(site: SiteId, vendor: VendorId): ExtensionPermission[] { + const permissions = [ + this.sitePermissions.get(site), + this.vendorPermissions.get(vendor), + this.allPermission, + // Need to cast away `undefined` because typescript isn't + // aware that the filter eliminates undefined elements + ].filter((p) => !!p) as ExtensionPermission[]; + + return permissions; + } + + extensions(): ReadonlyArray<{ + extension: ExtensionMetadata; + permissions: ExtensionPermission[]; + }> { + const extensions = []; + for (const extension of this.extensionRegistrations) { + const permissions = this.getPermissions(extension.site.id, extension.product.vendor.id); + + extensions.push({ extension, permissions }); + } + + return extensions; + } + + build(id: SiteId): ExtensionSite | undefined { + const site = this.siteRegistrations.get(id); + if (!site) { + return undefined; + } + + if (this.allPermission === "deny") { + return new ExtensionSite(site, new Map()); + } + + const extensions = new Map(); + const entries = this.extensionsBySiteByVendor.get(id)?.entries() ?? ([] as const); + for (const [vendor, index] of entries) { + const permissions = this.getPermissions(id, vendor); + + const extension = evaluate(permissions, this.extensionRegistrations[index]); + if (extension) { + extensions.set(vendor, extension); + } + } + + const extensionSite = new ExtensionSite(site, extensions); + return extensionSite; + } +} + +function evaluate( + permissions: ExtensionPermission[], + value: ExtensionMetadata, +): ExtensionMetadata | undefined { + // deny always wins + if (permissions.includes("deny")) { + return undefined; + } + + // allow overrides implicit permissions + if (permissions.includes("allow")) { + return value; + } + + // none permission becomes a deny + if (permissions.includes("none")) { + return undefined; + } + + // default permission becomes an allow + if (permissions.includes("default")) { + return value; + } + + // if no permission is recognized, throw. This code is unreachable. + throw new Error("failed to recognize any permissions"); +} diff --git a/libs/common/src/tools/extension/type.ts b/libs/common/src/tools/extension/type.ts new file mode 100644 index 00000000000..f37d4ff8e53 --- /dev/null +++ b/libs/common/src/tools/extension/type.ts @@ -0,0 +1,109 @@ +import { Opaque } from "type-fest"; + +import { Site, Field, Permission } from "./data"; + +/** well-known name for a feature extensible through an extension. */ +export type SiteId = keyof typeof Site; + +/** well-known name for a field surfaced from an extension site to a vendor. */ +export type FieldId = keyof typeof Field; + +/** Identifies a vendor extending bitwarden */ +export type VendorId = Opaque<"vendor", string>; + +/** uniquely identifies an extension. */ +export type ExtensionId = { site: SiteId; vendor: VendorId }; + +/** Permission levels for metadata. */ +export type ExtensionPermission = keyof typeof Permission; + +/** The capabilities and descriptive content for an extension */ +export type SiteMetadata = { + /** Uniquely identifies the extension site. */ + id: SiteId; + + /** Lists the fields disclosed by the extension to the vendor */ + availableFields: FieldId[]; +}; + +/** The capabilities and descriptive content for an extension */ +export type VendorMetadata = { + /** Uniquely identifies the vendor. */ + id: VendorId; + + /** Brand name of the service providing the extension. */ + name: string; +}; + +type TokenHeader = + | { + /** Transmit the token as the value of an `Authentication` header */ + authentication: true; + } + | { + /** Transmit the token as an `Authorization` header and a formatted value + * * `bearer` uses OAUTH-2.0 bearer token format + * * `token` prefixes the token with "Token" + * * `basic-username` uses HTTP Basic authentication format, encoding the + * token as the username. + */ + authorization: "bearer" | "token" | "basic-username"; + }; + +/** Catalogues an extension's hosting status. + * selfHost: "never" always uses the service's base URL + * selfHost: "maybe" allows the user to override the service's + * base URL with their own. + * selfHost: "always" requires a base URL. + */ +export type ApiHost = TokenHeader & + ( + | { selfHost: "never"; baseUrl: string } + | { selfHost: "maybe"; baseUrl: string } + | { selfHost: "always" } + ); + +/** Describes a branded product */ +export type ProductMetadata = { + /** The vendor providing the extension */ + vendor: VendorMetadata; + + /** The branded name of the product, if it varies from the Vendor name */ + name?: string; +}; + +/** Describes an extension provided by a vendor */ +export type ExtensionMetadata = { + /** The part of Bitwarden extended by the vendor's services */ + readonly site: Readonly; + + /** Product description */ + readonly product: Readonly; + + /** Hosting provider capabilities required by the extension */ + readonly host: Readonly; + + /** Lists the fields disclosed by the extension to the vendor. + * This should be a subset of the `availableFields` listed in + * the extension. + */ + readonly requestedFields: ReadonlyArray>; +}; + +/** Identifies a collection of extensions. + */ +export type ExtensionSet = + | { + /** A set of extensions sharing an extension point */ + site: SiteId; + } + | { + /** A set of extensions sharing a vendor */ + vendor: VendorId; + } + | { + /** The total set of extensions. This is used to set a categorical + * rule affecting all extensions. + */ + all: true; + }; diff --git a/libs/common/src/tools/extension/vendor/addyio.ts b/libs/common/src/tools/extension/vendor/addyio.ts new file mode 100644 index 00000000000..c33abd570ad --- /dev/null +++ b/libs/common/src/tools/extension/vendor/addyio.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const AddyIo: VendorMetadata = { + id: Vendor.addyio, + name: "Addy.io", +}; + +export const AddyIoExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: AddyIo, + }, + host: { + authorization: "bearer", + selfHost: "maybe", + baseUrl: "https://app.addy.io", + }, + requestedFields: [Field.token, Field.baseUrl, Field.domain], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/bitwarden.ts b/libs/common/src/tools/extension/vendor/bitwarden.ts new file mode 100644 index 00000000000..7f659c2d07f --- /dev/null +++ b/libs/common/src/tools/extension/vendor/bitwarden.ts @@ -0,0 +1,8 @@ +import { VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const Bitwarden: VendorMetadata = Object.freeze({ + id: Vendor.bitwarden, + name: "Bitwarden", +}); diff --git a/libs/common/src/tools/extension/vendor/data.ts b/libs/common/src/tools/extension/vendor/data.ts new file mode 100644 index 00000000000..7f0802ef82f --- /dev/null +++ b/libs/common/src/tools/extension/vendor/data.ts @@ -0,0 +1,11 @@ +import { VendorId } from "../type"; + +export const Vendor = Object.freeze({ + addyio: "addyio" as VendorId, + bitwarden: "bitwarden" as VendorId, // RESERVED + duckduckgo: "duckduckgo" as VendorId, + fastmail: "fastmail" as VendorId, + forwardemail: "forwardemail" as VendorId, + mozilla: "mozilla" as VendorId, + simplelogin: "simplelogin" as VendorId, +} as const); diff --git a/libs/common/src/tools/extension/vendor/duckduckgo.ts b/libs/common/src/tools/extension/vendor/duckduckgo.ts new file mode 100644 index 00000000000..ca4634192f5 --- /dev/null +++ b/libs/common/src/tools/extension/vendor/duckduckgo.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const DuckDuckGo: VendorMetadata = { + id: Vendor.duckduckgo, + name: "DuckDuckGo", +}; + +export const DuckDuckGoExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: DuckDuckGo, + }, + host: { + authorization: "bearer", + selfHost: "never", + baseUrl: "https://quack.duckduckgo.com/api", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/fastmail.ts b/libs/common/src/tools/extension/vendor/fastmail.ts new file mode 100644 index 00000000000..e6fb9ec16be --- /dev/null +++ b/libs/common/src/tools/extension/vendor/fastmail.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const Fastmail: VendorMetadata = { + id: Vendor.fastmail, + name: "Fastmail", +}; + +export const FastmailExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: Fastmail, + }, + host: { + authorization: "bearer", + selfHost: "maybe", + baseUrl: "https://api.fastmail.com", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/forwardemail.ts b/libs/common/src/tools/extension/vendor/forwardemail.ts new file mode 100644 index 00000000000..4fbc8c139b1 --- /dev/null +++ b/libs/common/src/tools/extension/vendor/forwardemail.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const ForwardEmail: VendorMetadata = { + id: Vendor.forwardemail, + name: "Forward Email", +}; + +export const ForwardEmailExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: ForwardEmail, + }, + host: { + authorization: "basic-username", + selfHost: "never", + baseUrl: "https://api.forwardemail.net", + }, + requestedFields: [Field.domain, Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/index.ts b/libs/common/src/tools/extension/vendor/index.ts new file mode 100644 index 00000000000..3bac78c80db --- /dev/null +++ b/libs/common/src/tools/extension/vendor/index.ts @@ -0,0 +1,30 @@ +import { deepFreeze } from "../../util"; + +import { AddyIo, AddyIoExtensions } from "./addyio"; +import { Bitwarden } from "./bitwarden"; +import { DuckDuckGo, DuckDuckGoExtensions } from "./duckduckgo"; +import { Fastmail, FastmailExtensions } from "./fastmail"; +import { ForwardEmail, ForwardEmailExtensions } from "./forwardemail"; +import { Mozilla, MozillaExtensions } from "./mozilla"; +import { SimpleLogin, SimpleLoginExtensions } from "./simplelogin"; + +export const Vendors = deepFreeze([ + AddyIo, + Bitwarden, + DuckDuckGo, + Fastmail, + ForwardEmail, + Mozilla, + SimpleLogin, +]); + +export const VendorExtensions = deepFreeze( + [ + AddyIoExtensions, + DuckDuckGoExtensions, + FastmailExtensions, + ForwardEmailExtensions, + MozillaExtensions, + SimpleLoginExtensions, + ].flat(), +); diff --git a/libs/common/src/tools/extension/vendor/mozilla.ts b/libs/common/src/tools/extension/vendor/mozilla.ts new file mode 100644 index 00000000000..b02b97d8777 --- /dev/null +++ b/libs/common/src/tools/extension/vendor/mozilla.ts @@ -0,0 +1,26 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const Mozilla: VendorMetadata = { + id: Vendor.mozilla, + name: "Mozilla", +}; + +export const MozillaExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: Mozilla, + name: "Firefox Relay", + }, + host: { + authorization: "token", + selfHost: "never", + baseUrl: "https://relay.firefox.com/api", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/readme.md b/libs/common/src/tools/extension/vendor/readme.md new file mode 100644 index 00000000000..507769edd4e --- /dev/null +++ b/libs/common/src/tools/extension/vendor/readme.md @@ -0,0 +1,33 @@ +# Vendors + +This folder contains vendor-specific logic that extends the +Bitwarden password manager. + +## Vendor IDs + +A vendor's ID is used to identify and trace the code provided by +a vendor across Bitwarden. There are a few rules that vendor ids +must follow: + +1. They should be human-readable. (No UUIDs.) +2. They may only contain lowercase ASCII characters and numbers. +3. They must retain backwards compatibility with prior versions. + +As such, any given ID may not not match the vendor's present +brand identity. Said branding may be stored in `VendorMetadata.name`. + +## Core files + +There are 4 vendor-independent files in this directory. + +- `data.ts` - core metadata used for system initialization +- `index.ts` - exports vendor metadata +- `README.md` - this file + +## Vendor definitions + +Each vendor should have one and only one definition, whose name +MUST match their `VendorId`. The vendor is free to use either a +single file (e.g. `bitwarden.ts`) or a folder containing multiple +files (e.g. `bitwarden/extension.ts`, `bitwarden/forwarder.ts`) to +host their files. diff --git a/libs/common/src/tools/extension/vendor/simplelogin.ts b/libs/common/src/tools/extension/vendor/simplelogin.ts new file mode 100644 index 00000000000..21ee969cebb --- /dev/null +++ b/libs/common/src/tools/extension/vendor/simplelogin.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const SimpleLogin: VendorMetadata = { + id: Vendor.simplelogin, + name: "SimpleLogin", +}; + +export const SimpleLoginExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: SimpleLogin, + }, + host: { + authentication: true, + selfHost: "maybe", + baseUrl: "https://app.simplelogin.io", + }, + requestedFields: [Field.baseUrl, Field.token, Field.domain], + }, +]; diff --git a/libs/common/src/tools/integration/integration-context.spec.ts b/libs/common/src/tools/integration/integration-context.spec.ts index 58115c783c7..42581c08dee 100644 --- a/libs/common/src/tools/integration/integration-context.spec.ts +++ b/libs/common/src/tools/integration/integration-context.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; import { IntegrationContext } from "./integration-context"; import { IntegrationId } from "./integration-id"; diff --git a/libs/common/src/tools/integration/integration-context.ts b/libs/common/src/tools/integration/integration-context.ts index f30810ff81a..40648df6803 100644 --- a/libs/common/src/tools/integration/integration-context.ts +++ b/libs/common/src/tools/integration/integration-context.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { Utils } from "../../platform/misc/utils"; import { IntegrationMetadata } from "./integration-metadata"; import { ApiSettings, IntegrationRequest } from "./rpc"; diff --git a/libs/common/src/tools/integration/rpc/rest-client.spec.ts b/libs/common/src/tools/integration/rpc/rest-client.spec.ts index e113ab9ff43..9b76d305e6f 100644 --- a/libs/common/src/tools/integration/rpc/rest-client.spec.ts +++ b/libs/common/src/tools/integration/rpc/rest-client.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiService } from "../../../abstractions/api.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; import { IntegrationRequest } from "./integration-request"; import { RestClient } from "./rest-client"; diff --git a/libs/common/src/tools/integration/rpc/rest-client.ts b/libs/common/src/tools/integration/rpc/rest-client.ts index 287bb4f2573..c42244166e7 100644 --- a/libs/common/src/tools/integration/rpc/rest-client.ts +++ b/libs/common/src/tools/integration/rpc/rest-client.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiService } from "../../../abstractions/api.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; import { IntegrationRequest } from "./integration-request"; import { JsonRpc } from "./rpc"; diff --git a/libs/common/src/tools/private-classifier.ts b/libs/common/src/tools/private-classifier.ts index de21f78f1c6..58244ae9906 100644 --- a/libs/common/src/tools/private-classifier.ts +++ b/libs/common/src/tools/private-classifier.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Classifier } from "@bitwarden/common/tools/state/classifier"; +import { Classifier } from "./state/classifier"; export class PrivateClassifier implements Classifier, Data> { constructor(private keys: (keyof Jsonify)[] = undefined) {} diff --git a/libs/common/src/tools/public-classifier.ts b/libs/common/src/tools/public-classifier.ts index e7c8c24ba78..e036ebd1c42 100644 --- a/libs/common/src/tools/public-classifier.ts +++ b/libs/common/src/tools/public-classifier.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Classifier } from "@bitwarden/common/tools/state/classifier"; +import { Classifier } from "./state/classifier"; export class PublicClassifier implements Classifier> { constructor(private keys: (keyof Jsonify)[]) {} diff --git a/libs/common/src/tools/rx.spec.ts b/libs/common/src/tools/rx.spec.ts index 9ce147a3ff4..2c433fef93b 100644 --- a/libs/common/src/tools/rx.spec.ts +++ b/libs/common/src/tools/rx.spec.ts @@ -56,7 +56,7 @@ describe("errorOnChange", () => { source$.complete(); - expect(complete).toBeTrue(); + expect(complete).toBe(true); }); it("errors when the input changes", async () => { diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 74c0e77b394..fcc273d41bb 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -1,12 +1,12 @@ import { mock } from "jest-mock-extended"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; -import { KeyService } from "../../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../../platform/services/container.service"; +import { UserKey } from "../../../../types/key"; import { SendType } from "../../enums/send-type"; import { SendData } from "../data/send.data"; @@ -123,7 +123,12 @@ describe("Send", () => { const view = await send.decrypt(); expect(text.decrypt).toHaveBeenNthCalledWith(1, "cryptoKey"); - expect(send.name.decrypt).toHaveBeenNthCalledWith(1, null, "cryptoKey"); + expect(send.name.decrypt).toHaveBeenNthCalledWith( + 1, + null, + "cryptoKey", + "Property: name; ObjectContext: No Domain Context", + ); expect(view).toMatchObject({ id: "id", diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 43115b65937..c2390d439e7 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -81,6 +81,8 @@ export class Send extends Domain { const sendKeyEncryptionKey = await keyService.getUserKey(); model.key = await encryptService.decryptToBytes(this.key, sendKeyEncryptionKey); model.cryptoKey = await keyService.makeSendKey(model.key); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // TODO: error? } diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index a6427824a64..570f3e746a0 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -22,11 +22,6 @@ export abstract class SendApiService { postSend: (request: SendRequest) => Promise; postFileTypeSend: (request: SendRequest) => Promise; postSendFile: (sendId: string, fileId: string, data: FormData) => Promise; - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - postSendFileLegacy: (data: FormData) => Promise; putSend: (id: string, request: SendRequest) => Promise; putSendRemovePassword: (id: string) => Promise; deleteSend: (id: string) => Promise; diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index ff71408bce3..f709553646f 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -5,7 +5,6 @@ import { FileUploadApiMethods, FileUploadService, } from "../../../platform/abstractions/file-upload/file-upload.service"; -import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SendType } from "../enums/send-type"; import { SendData } from "../models/data/send.data"; @@ -106,15 +105,6 @@ export class SendApiService implements SendApiServiceAbstraction { return this.apiService.send("POST", "/sends/" + sendId + "/file/" + fileId, data, true, false); } - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postSendFileLegacy(data: FormData): Promise { - const r = await this.apiService.send("POST", "/sends/file", data, true, true); - return new SendResponse(r); - } - async putSend(id: string, request: SendRequest): Promise { const r = await this.apiService.send("PUT", "/sends/" + id, request, true, true); return new SendResponse(r); @@ -173,9 +163,7 @@ export class SendApiService implements SendApiServiceAbstraction { this.generateMethods(uploadDataResponse, response), ); } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { - response = await this.legacyServerSendFileUpload(sendData, request); - } else if (e instanceof ErrorResponse) { + if (e instanceof ErrorResponse) { throw new Error((e as ErrorResponse).getSingleMessage()); } else { throw e; @@ -219,35 +207,4 @@ export class SendApiService implements SendApiServiceAbstraction { return this.deleteSend(sendId); }; } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async legacyServerSendFileUpload( - sendData: [Send, EncArrayBuffer], - request: SendRequest, - ): Promise { - const fd = new FormData(); - try { - const blob = new Blob([sendData[1].buffer], { type: "application/octet-stream" }); - fd.append("model", JSON.stringify(request)); - fd.append("data", blob, sendData[0].file.fileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append("model", JSON.stringify(request)); - fd.append( - "data", - Buffer.from(sendData[1].buffer) as any, - { - filepath: sendData[0].file.fileName.encryptedString, - contentType: "application/octet-stream", - } as any, - ); - } else { - throw e; - } - } - return await this.postSendFileLegacy(fd); - } } diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 5aca3a4b5c9..662fee02bbf 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -1,10 +1,8 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { KeyService } from "@bitwarden/key-management"; -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeActiveUserState, @@ -13,12 +11,14 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EnvironmentService } from "../../../platform/abstractions/environment.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; +import { SelfHostedEnvironment } from "../../../platform/services/default-environment.service"; import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { SendType } from "../enums/send-type"; diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index 6a50a1dd668..8111f6f9f17 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -1,9 +1,8 @@ import { BehaviorSubject, of, Subject } from "rxjs"; -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec"; +import { GENERATOR_DISK, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; import { UserBound } from "../dependencies"; import { PrivateClassifier } from "../private-classifier"; diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index d508cf71663..c00560304e4 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -26,10 +26,9 @@ import { skip, } from "rxjs"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - +import { EncString } from "../../platform/models/domain/enc-string"; +import { SingleUserState, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; import { UserBound } from "../dependencies"; import { anyComplete, errorOnChange, ready, withLatestReady } from "../rx"; diff --git a/libs/common/src/tools/util.ts b/libs/common/src/tools/util.ts new file mode 100644 index 00000000000..9a3a14c1c83 --- /dev/null +++ b/libs/common/src/tools/util.ts @@ -0,0 +1,19 @@ +/** Recursively freeze an object's own keys + * @param value the value to freeze + * @returns `value` + * @remarks this function is derived from MDN's `deepFreeze`, which + * has been committed to the public domain. + */ +export function deepFreeze(value: T): Readonly { + const keys = Reflect.ownKeys(value) as (keyof T)[]; + + for (const key of keys) { + const own = value[key]; + + if ((own && typeof own === "object") || typeof own === "function") { + deepFreeze(own); + } + } + + return Object.freeze(value); +} diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 59dbe28f907..5ad498c115a 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -10,3 +10,4 @@ export type PolicyId = Opaque; export type CipherId = Opaque; export type SendId = Opaque; export type IndexedEntityId = Opaque; +export type SecurityTaskId = Opaque; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 870cb8b3d73..2e34f0ac601 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { Observable } from "rxjs"; -import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; @@ -11,6 +10,7 @@ import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid import { UserKey } from "../../types/key"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; +import { LocalData } from "../models/data/local.data"; import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherWithIdRequest } from "../models/request/cipher-with-id.request"; @@ -26,6 +26,12 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$: Observable; clearCache: (userId?: string) => Promise; encrypt: ( model: CipherView, @@ -87,7 +93,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + ) => Promise; shareManyWithServer: ( ciphers: CipherView[], organizationId: string, diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index 859f2183edb..1bb4a52e929 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -1,12 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { UserId } from "@bitwarden/common/types/guid"; +import { UserId } from "../../../types/guid"; +import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; export class FolderApiServiceAbstraction { - save: (folder: Folder, userId: UserId) => Promise; + save: (folder: Folder, userId: UserId) => Promise; delete: (id: string, userId: UserId) => Promise; get: (id: string) => Promise; deleteAll: (userId: UserId) => Promise; diff --git a/libs/common/src/vault/icon/build-cipher-icon.ts b/libs/common/src/vault/icon/build-cipher-icon.ts index 78e6ecd7b4f..5775bc7f55e 100644 --- a/libs/common/src/vault/icon/build-cipher-icon.ts +++ b/libs/common/src/vault/icon/build-cipher-icon.ts @@ -43,10 +43,18 @@ export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, show isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1; } + if (isWebsite && (hostnameUri.endsWith(".onion") || hostnameUri.endsWith(".i2p"))) { + image = null; + fallbackImage = "images/bwi-globe.png"; + break; + } + if (showFavicon && isWebsite) { try { image = `${iconsServerUrl}/${Utils.getHostname(hostnameUri)}/icon.png`; fallbackImage = "images/bwi-globe.png"; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore error since the fallback icon will be shown if image is null. } diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 14dec8dea0c..8cae7170738 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; @@ -101,7 +103,7 @@ describe("Attachment", () => { it("uses the provided key without depending on KeyService", async () => { const providedKey = mock(); - await attachment.decrypt(null, providedKey); + await attachment.decrypt(null, "", providedKey); expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey); @@ -111,7 +113,7 @@ describe("Attachment", () => { const orgKey = mock(); keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); - await attachment.decrypt("orgId", null); + await attachment.decrypt("orgId", "", null); expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId"); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey); @@ -121,7 +123,7 @@ describe("Attachment", () => { const userKey = mock(); keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); - await attachment.decrypt(null, null); + await attachment.decrypt(null, "", null); expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 1178f441c5e..4eee0307746 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -38,7 +38,11 @@ export class Attachment extends Domain { ); } - async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { const view = await this.decryptObj( new AttachmentView(this), { @@ -46,6 +50,7 @@ export class Attachment extends Domain { }, orgId, encKey, + "DomainType: Attachment; " + context, ); if (this.key != null) { @@ -64,6 +69,8 @@ export class Attachment extends Domain { const encryptService = Utils.getContainerService().getEncryptService(); const decValue = await encryptService.decryptToBytes(this.key, encKey); return new SymmetricCryptoKey(decValue); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // TODO: error? } diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 739cbf78465..fccfe3f595b 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -37,7 +37,11 @@ export class Card extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new CardView(), { @@ -50,6 +54,7 @@ export class Card extends Domain { }, orgId, encKey, + "DomainType: Card; " + context, ); } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 509a17a8a0e..dd79da3086e 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; @@ -10,6 +10,7 @@ import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; +import { UserId } from "../../../types/guid"; import { CipherService } from "../../abstractions/cipher.service"; import { FieldType, SecureNoteType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index d7355ce5202..d82f4585e65 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -136,7 +136,17 @@ export class Cipher extends Domain implements Decryptable { if (this.key != null) { const encryptService = Utils.getContainerService().getEncryptService(); - encKey = new SymmetricCryptoKey(await encryptService.decryptToBytes(this.key, encKey)); + const keyBytes = await encryptService.decryptToBytes( + this.key, + encKey, + `Cipher Id: ${this.id}; Content: CipherKey; IsEncryptedByOrgKey: ${this.organizationId != null}`, + ); + if (keyBytes == null) { + model.name = "[error: cannot decrypt]"; + model.decryptionFailure = true; + return model; + } + encKey = new SymmetricCryptoKey(keyBytes); bypassValidation = false; } @@ -152,19 +162,36 @@ export class Cipher extends Domain implements Decryptable { switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt(this.organizationId, bypassValidation, encKey); + model.login = await this.login.decrypt( + this.organizationId, + bypassValidation, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey); + model.secureNote = await this.secureNote.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId, encKey); + model.card = await this.card.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); break; case CipherType.Identity: - model.identity = await this.identity.decrypt(this.organizationId, encKey); + model.identity = await this.identity.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.SshKey: - model.sshKey = await this.sshKey.decrypt(this.organizationId, encKey); + model.sshKey = await this.sshKey.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; default: break; @@ -175,7 +202,7 @@ export class Cipher extends Domain implements Decryptable { await this.attachments.reduce((promise, attachment) => { return promise .then(() => { - return attachment.decrypt(this.organizationId, encKey); + return attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); }) .then((decAttachment) => { attachments.push(decAttachment); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index e2b7aef52f0..570e6c0b4d5 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -61,7 +61,11 @@ export class Identity extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context: string = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new IdentityView(), { @@ -86,6 +90,7 @@ export class Identity extends Domain { }, orgId, encKey, + "DomainType: Identity; " + context, ); } diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 0d7380e034d..36782a81502 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -33,7 +33,11 @@ export class LoginUri extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context: string = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new LoginUriView(this), { @@ -41,6 +45,7 @@ export class LoginUri extends Domain { }, orgId, encKey, + context, ); } diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index a0a61a9b857..f9a85cd818e 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -55,6 +55,7 @@ export class Login extends Domain { async decrypt( orgId: string, bypassValidation: boolean, + context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { const view = await this.decryptObj( @@ -66,6 +67,7 @@ export class Login extends Domain { }, orgId, encKey, + `DomainType: Login; ${context}`, ); if (this.uris != null) { @@ -76,7 +78,7 @@ export class Login extends Domain { continue; } - const uri = await this.uris[i].decrypt(orgId, encKey); + const uri = await this.uris[i].decrypt(orgId, context, encKey); // URIs are shared remotely after decryption // we need to validate that the string hasn't been changed by a compromised server // This validation is tied to the existence of cypher.key for backwards compatibility diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index 4c4f465654e..48063f495f0 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -32,6 +32,7 @@ export class Password extends Domain { }, orgId, encKey, + "DomainType: PasswordHistory", ); } diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 4769ad062d9..693ae38d9fb 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -20,8 +20,12 @@ export class SecureNote extends Domain { this.type = obj.type; } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { - return Promise.resolve(new SecureNoteView(this)); + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { + return new SecureNoteView(this); } toSecureNoteData(): SecureNoteData { diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index 3a79c1f0022..b4df172e543 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; - import Domain from "../../../platform/models/domain/domain-base"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SshKeyData } from "../data/ssh-key.data"; import { SshKeyView } from "../view/ssh-key.view"; @@ -32,7 +31,11 @@ export class SshKey extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new SshKeyView(), { @@ -42,6 +45,7 @@ export class SshKey extends Domain { }, orgId, encKey, + "DomainType: SshKey; " + context, ); } diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 6416ab31970..20dbd23065c 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -46,6 +46,11 @@ export class CipherView implements View, InitializerMetadata { deletedDate: Date = null; reprompt: CipherRepromptType = CipherRepromptType.None; + /** + * Flag to indicate if the cipher decryption failed. + */ + decryptionFailure = false; + constructor(c?: Cipher) { if (!c) { return; @@ -150,6 +155,8 @@ export class CipherView implements View, InitializerMetadata { return null; } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars const item = this.item; return this.item[linkedFieldOption.propertyKey as keyof typeof item]; } diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index a6b7a0c0a22..315adb87c75 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -142,6 +142,8 @@ export class LoginUriView implements View { try { const regex = new RegExp(this.uri, "i"); return regex.test(targetUri); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Invalid regex return false; diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index cccd29ad697..37ddfdeaeeb 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -1,11 +1,13 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { Observable, firstValueFrom, of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; import { CipherView } from "../models/view/cipher.view"; import { @@ -18,6 +20,8 @@ describe("CipherAuthorizationService", () => { const mockCollectionService = mock(); const mockOrganizationService = mock(); + const mockUserId = Utils.newGuid() as UserId; + let mockAccountService: FakeAccountService; // Mock factories const createMockCipher = ( @@ -42,6 +46,7 @@ describe("CipherAuthorizationService", () => { isAdmin = false, editAnyCollection = false, } = {}) => ({ + id: "org1", allowAdminAccessToAllCollectionItems, canEditAllCiphers, canEditUnassignedCiphers, @@ -53,9 +58,11 @@ describe("CipherAuthorizationService", () => { beforeEach(() => { jest.clearAllMocks(); + mockAccountService = mockAccountServiceWith(mockUserId); cipherAuthorizationService = new DefaultCipherAuthorizationService( mockCollectionService, mockOrganizationService, + mockAccountService, ); }); @@ -72,7 +79,9 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); @@ -83,11 +92,13 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => { const cipher = createMockCipher("org1", ["col1"]) as CipherView; const organization = createMockOrganization({ canEditAllCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); - expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1"); + expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId); done(); }); }); @@ -95,7 +106,7 @@ describe("CipherAuthorizationService", () => { it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: false }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(false); @@ -106,8 +117,8 @@ describe("CipherAuthorizationService", () => { it("should return true if activeCollectionId is provided and has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", true), @@ -132,8 +143,8 @@ describe("CipherAuthorizationService", () => { it("should return false if activeCollectionId is provided and manage permission is not present", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -157,8 +168,8 @@ describe("CipherAuthorizationService", () => { it("should return true if any collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -182,8 +193,8 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -216,7 +227,9 @@ describe("CipherAuthorizationService", () => { it("should return true for admin users", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ isAdmin: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -227,7 +240,9 @@ describe("CipherAuthorizationService", () => { it("should return true for custom user with canEditAnyCollection", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ editAnyCollection: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -240,7 +255,9 @@ describe("CipherAuthorizationService", () => { it("should return true if at least one cipher collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", true), @@ -257,7 +274,9 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", false), diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 260d1eeece7..fbee3ed8622 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -4,6 +4,7 @@ import { map, Observable, of, shareReplay, switchMap } from "rxjs"; 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 { CollectionId } from "@bitwarden/common/types/guid"; import { Cipher } from "../models/domain/cipher"; @@ -51,8 +52,14 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer constructor( private collectionService: CollectionService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} + private organization$ = (cipher: CipherLike) => + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + map((orgs) => orgs.find((org) => org.id === cipher.organizationId)), + ); /** * * {@link CipherAuthorizationService.canDeleteCipher$} @@ -66,7 +73,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { if (isAdminConsoleAction) { // If the user is an admin, they can delete an unassigned cipher @@ -104,7 +111,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { // Admins and custom users can always clone when in the Admin Console if ( diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 6b225af0d84..0d6578f165d 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherDecryptionKeys, KeyService, @@ -15,6 +15,7 @@ import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; +import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -359,6 +360,7 @@ describe("Cipher Service", () => { const originalUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; let decryptedCiphers: BehaviorSubject>; + let failedCiphers: BehaviorSubject; let encryptedKey: EncString; beforeEach(() => { @@ -385,6 +387,7 @@ describe("Cipher Service", () => { Cipher2: cipher2, }); cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))); + cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject([]); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Cipher Key"); @@ -413,5 +416,16 @@ describe("Cipher Service", () => { "New user key is required to rotate ciphers", ); }); + + it("throws if the user has any failed to decrypt ciphers", async () => { + const badCipher = new CipherView(cipherObj); + badCipher.id = "Cipher 3"; + badCipher.organizationId = null; + badCipher.decryptionFailure = true; + failedCiphers.next([badCipher]); + await expect( + cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId), + ).rejects.toThrow("Cannot rotate ciphers when decryption failures are present"); + }); }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8711496b374..b1cdf72e08e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -7,25 +7,27 @@ import { map, merge, Observable, + of, shareReplay, Subject, switchMap, } from "rxjs"; import { SemVer } from "semver"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; +import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -79,6 +81,7 @@ import { ADD_EDIT_CIPHER_INFO_KEY, DECRYPTED_CIPHERS, ENCRYPTED_CIPHERS, + FAILED_DECRYPTED_CIPHERS, LOCAL_DATA_KEY, } from "./key-state/ciphers.state"; @@ -109,9 +112,17 @@ export class CipherService implements CipherServiceAbstraction { cipherViews$: Observable; addEditCipherInfo$: Observable; + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$: Observable; + private localDataState: ActiveUserState>; private encryptedCiphersState: ActiveUserState>; private decryptedCiphersState: ActiveUserState>; + private failedToDecryptCiphersState: ActiveUserState; private addEditCipherInfoState: ActiveUserState; constructor( @@ -132,6 +143,7 @@ export class CipherService implements CipherServiceAbstraction { this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); + this.failedToDecryptCiphersState = this.stateProvider.getActive(FAILED_DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); @@ -143,6 +155,13 @@ export class CipherService implements CipherServiceAbstraction { switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())), shareReplay({ bufferSize: 1, refCount: true }), ); + + this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe( + filter((ciphers) => ciphers != null), + switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))), + shareReplay({ bufferSize: 1, refCount: true }), + ); + this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } @@ -162,6 +181,10 @@ export class CipherService implements CipherServiceAbstraction { } } + async setFailedDecryptedCiphers(cipherViews: CipherView[], userId: UserId) { + await this.stateProvider.setUserState(FAILED_DECRYPTED_CIPHERS, cipherViews, userId); + } + private async setDecryptedCiphers(value: CipherView[], userId: UserId) { const cipherViews: { [id: string]: CipherView } = {}; value?.forEach((c) => { @@ -378,7 +401,7 @@ export class CipherService implements CipherServiceAbstraction { */ @sequentialize(() => "getAllDecrypted") async getAllDecrypted(): Promise { - let decCiphers = await this.getDecryptedCiphers(); + const decCiphers = await this.getDecryptedCiphers(); if (decCiphers != null && decCiphers.length !== 0) { await this.reindexCiphers(); return await this.getDecryptedCiphers(); @@ -390,10 +413,15 @@ export class CipherService implements CipherServiceAbstraction { return []; } - decCiphers = await this.decryptCiphers(await this.getAll(), activeUserId); + const [newDecCiphers, failedCiphers] = await this.decryptCiphers( + await this.getAll(), + activeUserId, + ); - await this.setDecryptedCipherCache(decCiphers, activeUserId); - return decCiphers; + await this.setDecryptedCipherCache(newDecCiphers, activeUserId); + await this.setFailedDecryptedCiphers(failedCiphers, activeUserId); + + return newDecCiphers; } private async getDecryptedCiphers() { @@ -402,7 +430,17 @@ export class CipherService implements CipherServiceAbstraction { ); } - private async decryptCiphers(ciphers: Cipher[], userId: UserId) { + /** + * Decrypts the provided ciphers using the provided user's keys. + * @param ciphers + * @param userId + * @returns Two cipher arrays, the first containing successfully decrypted ciphers and the second containing ciphers that failed to decrypt. + * @private + */ + private async decryptCiphers( + ciphers: Cipher[], + userId: UserId, + ): Promise<[CipherView[], CipherView[]]> { const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { @@ -420,7 +458,7 @@ export class CipherService implements CipherServiceAbstraction { {} as Record, ); - const decCiphers = ( + const allCipherViews = ( await Promise.all( Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { @@ -440,7 +478,18 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - return decCiphers; + // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt + return allCipherViews.reduce( + (acc, c) => { + if (c.decryptionFailure) { + acc[1].push(c); + } else { + acc[0].push(c); + } + return acc; + }, + [[], []] as [CipherView[], CipherView[]], + ); } private async reindexCiphers() { @@ -737,7 +786,7 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, collectionIds: string[], userId: UserId, - ): Promise { + ): Promise { const attachmentPromises: Promise[] = []; if (cipher.attachments != null) { cipher.attachments.forEach((attachment) => { @@ -757,6 +806,7 @@ export class CipherService implements CipherServiceAbstraction { const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); await this.upsert(data); + return new Cipher(data, cipher.localData); } async shareManyWithServer( @@ -1272,10 +1322,15 @@ export class CipherService implements CipherServiceAbstraction { let encryptedCiphers: CipherWithIdRequest[] = []; const ciphers = await firstValueFrom(this.cipherViews$); + const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$); if (!ciphers) { return encryptedCiphers; } + if (failedCiphers.length > 0) { + throw new Error("Cannot rotate ciphers when decryption failures are present"); + } + const userCiphers = ciphers.filter((c) => c.organizationId == null); if (userCiphers.length === 0) { return encryptedCiphers; @@ -1636,6 +1691,7 @@ export class CipherService implements CipherServiceAbstraction { private async clearDecryptedCiphersState(userId: UserId) { await this.setDecryptedCiphers(null, userId); + await this.setFailedDecryptedCiphers(null, userId); this.clearSortedCiphers(); } diff --git a/libs/common/src/vault/services/folder/folder-api.service.ts b/libs/common/src/vault/services/folder/folder-api.service.ts index 24831393668..fe9c3218a84 100644 --- a/libs/common/src/vault/services/folder/folder-api.service.ts +++ b/libs/common/src/vault/services/folder/folder-api.service.ts @@ -1,6 +1,5 @@ -import { UserId } from "@bitwarden/common/types/guid"; - import { ApiService } from "../../../abstractions/api.service"; +import { UserId } from "../../../types/guid"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; import { FolderData } from "../../../vault/models/data/folder.data"; @@ -14,7 +13,7 @@ export class FolderApiService implements FolderApiServiceAbstraction { private apiService: ApiService, ) {} - async save(folder: Folder, userId: UserId): Promise { + async save(folder: Folder, userId: UserId): Promise { const request = new FolderRequest(folder); let response: FolderResponse; @@ -27,6 +26,7 @@ export class FolderApiService implements FolderApiServiceAbstraction { const data = new FolderData(response); await this.folderService.upsert(data, userId); + return data; } async delete(id: string, userId: UserId): Promise { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 612cd83d99b..cc3aa1946ca 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString } from "../../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; @@ -75,7 +77,7 @@ describe("Folder Service", () => { const result = await firstValueFrom(folderService.folders$(mockUserId)); expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: "1", name: makeEncString("ENC_STRING_1") }, { id: "2", name: makeEncString("ENC_STRING_2") }, ]); @@ -96,7 +98,7 @@ describe("Folder Service", () => { const result = await firstValueFrom(folderService.folderViews$(mockUserId)); expect(result.length).toBe(3); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: "1", name: "DEC" }, { id: "2", name: "DEC" }, { name: "No Folder" }, diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 3aac5374fcb..c21a92fd894 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,9 +2,10 @@ // @ts-strict-ignore import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - +import { EncryptService } from ".././../../platform/abstractions/encrypt.service"; +import { Utils } from ".././../../platform/misc/utils"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/vault/services/key-state/ciphers.state.ts b/libs/common/src/vault/services/key-state/ciphers.state.ts index d9cedb89027..d85e625243e 100644 --- a/libs/common/src/vault/services/key-state/ciphers.state.ts +++ b/libs/common/src/vault/services/key-state/ciphers.state.ts @@ -28,6 +28,15 @@ export const DECRYPTED_CIPHERS = UserKeyDefinition.record( }, ); +export const FAILED_DECRYPTED_CIPHERS = UserKeyDefinition.array( + CIPHERS_MEMORY, + "failedDecryptedCiphers", + { + deserializer: (cipher: Jsonify) => CipherView.fromJSON(cipher), + clearOn: ["logout", "lock"], + }, +); + export const LOCAL_DATA_KEY = new UserKeyDefinition>( CIPHERS_DISK_LOCAL, "localData", diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index 99ad8e5ae35..b3e61f5bf31 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -6,7 +6,7 @@ import { FolderView } from "../../models/view/folder.view"; export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( FOLDER_DISK, - "folder", + "folders", { deserializer: (obj: Jsonify) => FolderData.fromJSON(obj), clearOn: ["logout"], diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index 27d8acbd360..2d1379f9c5f 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -1,5 +1,19 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + // TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved + "@bitwarden/common/*": ["../common/src/*"], + // TODO: Remove once billing stops depending on components + "@bitwarden/components": ["../components/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + // TODO: Remove once billing stops depending on components + "@bitwarden/ui-common": ["../ui/common/src"] + } + }, "include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"], "exclude": ["node_modules", "dist"] } diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts index c8e9d3d2fe3..ef7ba68b65c 100644 --- a/libs/components/src/a11y/a11y-grid.directive.ts +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -86,6 +86,8 @@ export class A11yGridDirective implements AfterViewInit { }); this.getActiveCellContent().tabIndex = 0; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // eslint-disable-next-line no-console console.error("Unable to initialize grid"); diff --git a/libs/angular/src/directives/a11y-title.directive.ts b/libs/components/src/a11y/a11y-title.directive.ts similarity index 98% rename from libs/angular/src/directives/a11y-title.directive.ts rename to libs/components/src/a11y/a11y-title.directive.ts index f5f016b93c0..c3833f42ed2 100644 --- a/libs/angular/src/directives/a11y-title.directive.ts +++ b/libs/components/src/a11y/a11y-title.directive.ts @@ -4,6 +4,7 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; @Directive({ selector: "[appA11yTitle]", + standalone: true, }) export class A11yTitleDirective implements OnInit { @Input() set appA11yTitle(title: string) { diff --git a/libs/components/src/a11y/index.ts b/libs/components/src/a11y/index.ts new file mode 100644 index 00000000000..6090fb65d4e --- /dev/null +++ b/libs/components/src/a11y/index.ts @@ -0,0 +1 @@ +export * from "./a11y-title.directive"; diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index fb94e43b196..7ddb32da281 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -5,6 +5,8 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { delay, of } from "rxjs"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { ButtonModule } from "../button"; diff --git a/libs/components/src/avatar/avatar.stories.ts b/libs/components/src/avatar/avatar.stories.ts index d3a00fbe344..19a6f86d89c 100644 --- a/libs/components/src/avatar/avatar.stories.ts +++ b/libs/components/src/avatar/avatar.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16994", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26525&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index ecf201f9842..86e9a84cb77 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -3,8 +3,9 @@ import { Component, Input, OnChanges } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BadgeModule, BadgeVariant } from "../badge"; -import { I18nPipe } from "../shared/i18n.pipe"; @Component({ selector: "bit-badge-list", diff --git a/libs/components/src/badge-list/badge-list.stories.ts b/libs/components/src/badge-list/badge-list.stories.ts index ede005f6fd6..f69ecde8377 100644 --- a/libs/components/src/badge-list/badge-list.stories.ts +++ b/libs/components/src/badge-list/badge-list.stories.ts @@ -33,7 +33,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26440&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index 5d697f8ad8f..bff9eec6163 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -18,7 +18,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26440&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index d3f64329978..a7b710d6a74 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -3,8 +3,9 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; type BannerTypes = "premium" | "info" | "warning" | "danger"; diff --git a/libs/components/src/banner/banner.stories.ts b/libs/components/src/banner/banner.stories.ts index c75d8b7034a..105d30bc04a 100644 --- a/libs/components/src/banner/banner.stories.ts +++ b/libs/components/src/banner/banner.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=2070%3A17207", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26720&t=b5tDKylm5sWm2yKo-4", }, }, args: { diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 9c8ccbccd3f..eb75a70ad75 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -35,6 +35,12 @@ export default { ], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26962&t=b5tDKylm5sWm2yKo-4", + }, + }, args: { items: [], show: 3, diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 2ed3774f814..6024b0559f2 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5115%3A26950", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28224&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index cb51e96e8da..3101d4316f1 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17484", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28300&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts index b33f5f4a198..3482eedfd54 100644 --- a/libs/components/src/card/card.stories.ts +++ b/libs/components/src/card/card.stories.ts @@ -34,6 +34,12 @@ export default { (story) => `
${story}
`, ), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28355&t=b5tDKylm5sWm2yKo-4", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index c322f29b957..908085bb6df 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -9,6 +9,8 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { BadgeModule } from "../badge"; @@ -81,7 +83,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-35837&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index 0f469f7d4b9..39543db5ed5 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -18,6 +18,8 @@ import { import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { compareValues } from "../../../common/src/platform/misc/compare-values"; import { ButtonModule } from "../button"; import { IconButtonModule } from "../icon-button"; diff --git a/libs/components/src/chip-select/chip-select.stories.ts b/libs/components/src/chip-select/chip-select.stories.ts index 598fa5b80be..e9b78235ccb 100644 --- a/libs/components/src/chip-select/chip-select.stories.ts +++ b/libs/components/src/chip-select/chip-select.stories.ts @@ -30,6 +30,12 @@ export default { ], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-29548&t=b5tDKylm5sWm2yKo-4", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/color-password/color-password.stories.ts b/libs/components/src/color-password/color-password.stories.ts index 07418cad721..bb835d97d4a 100644 --- a/libs/components/src/color-password/color-password.stories.ts +++ b/libs/components/src/color-password/color-password.stories.ts @@ -14,7 +14,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/6fvTDa3zfvgWdizLQ7nSTP/Numbered-Password", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21540-46261&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/angular/src/directives/copy-click.directive.spec.ts b/libs/components/src/copy-click/copy-click.directive.spec.ts similarity index 78% rename from libs/angular/src/directives/copy-click.directive.spec.ts rename to libs/components/src/copy-click/copy-click.directive.spec.ts index 29466f7fbe3..eab616b141e 100644 --- a/libs/angular/src/directives/copy-click.directive.spec.ts +++ b/libs/components/src/copy-click/copy-click.directive.spec.ts @@ -3,23 +3,32 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; + +import { ToastService } from "../"; import { CopyClickDirective } from "./copy-click.directive"; @Component({ template: ` - - - - + + + + `, + standalone: true, + imports: [CopyClickDirective], }) class TestCopyClickComponent { - @ViewChild("noToast") noToastButton: ElementRef; - @ViewChild("infoToast") infoToastButton: ElementRef; - @ViewChild("successToast") successToastButton: ElementRef; - @ViewChild("toastWithLabel") toastWithLabelButton: ElementRef; + @ViewChild("noToast") noToastButton!: ElementRef; + @ViewChild("infoToast") infoToastButton!: ElementRef; + @ViewChild("successToast") successToastButton!: ElementRef; + @ViewChild("toastWithLabel") toastWithLabelButton!: ElementRef; } describe("CopyClickDirective", () => { @@ -32,7 +41,7 @@ describe("CopyClickDirective", () => { showToast.mockClear(); await TestBed.configureTestingModule({ - declarations: [CopyClickDirective, TestCopyClickComponent], + imports: [TestCopyClickComponent], providers: [ { provide: I18nService, diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/components/src/copy-click/copy-click.directive.ts similarity index 93% rename from libs/angular/src/directives/copy-click.directive.ts rename to libs/components/src/copy-click/copy-click.directive.ts index cb346ebf8d3..f91366360c5 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/components/src/copy-click/copy-click.directive.ts @@ -4,11 +4,12 @@ import { Directive, HostListener, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; -import { ToastVariant } from "@bitwarden/components/src/toast/toast.component"; + +import { ToastService, ToastVariant } from "../"; @Directive({ selector: "[appCopyClick]", + standalone: true, }) export class CopyClickDirective { private _showToast = false; diff --git a/libs/components/src/copy-click/index.ts b/libs/components/src/copy-click/index.ts new file mode 100644 index 00000000000..82b971f1f5c --- /dev/null +++ b/libs/components/src/copy-click/index.ts @@ -0,0 +1 @@ +export * from "./copy-click.directive"; diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index 5e938412804..e7c5a17c308 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -19,7 +19,7 @@ interface Animal { } @Component({ - template: ``, + template: ``, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -42,8 +42,10 @@ class StoryDialogComponent { Animal: {{ animal }} - - + + `, @@ -91,7 +93,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-30495&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index ed47201805a..e9e3e898257 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -4,8 +4,9 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { CommonModule } from "@angular/common"; import { Component, HostBinding, Input } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; -import { I18nPipe } from "../../shared/i18n.pipe"; import { TypographyDirective } from "../../typography/typography.directive"; import { fadeIn } from "../animations"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 7cb6f40aa5b..03a88458f5a 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -68,7 +68,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-30495&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts index 50c0f8d2c25..87d6eb9fbfc 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -17,7 +17,7 @@ import { DialogModule } from "../../dialog.module";

{{ group.title }}

@for (dialog of group.dialogs; track dialog) { - } @@ -177,7 +177,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts index 665d658f8ee..680ebe9ed3b 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts @@ -17,7 +17,7 @@ interface Animal { } @Component({ - template: ``, + template: ``, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -41,8 +41,10 @@ class StoryDialogComponent { Animal: {{ animal }} - - + + `, @@ -87,7 +89,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts index d86b56101b0..50016ef358d 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts @@ -17,7 +17,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts index 974589a667c..bb3680c1f3b 100644 --- a/libs/components/src/disclosure/disclosure.stories.ts +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -13,6 +13,12 @@ export default { imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-47329&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/drawer/drawer-body.component.ts b/libs/components/src/drawer/drawer-body.component.ts new file mode 100644 index 00000000000..9bd2adcffbc --- /dev/null +++ b/libs/components/src/drawer/drawer-body.component.ts @@ -0,0 +1,36 @@ +import { CdkScrollable } from "@angular/cdk/scrolling"; +import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + +/** + * Body container for `bit-drawer` + */ +@Component({ + selector: "bit-drawer-body", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [], + host: { + class: + "tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200", + "[class.tw-border-t-secondary-300]": "isScrolled()", + }, + hostDirectives: [ + { + directive: CdkScrollable, + }, + ], + template: ` `, +}) +export class DrawerBodyComponent { + private scrollable = inject(CdkScrollable); + + /** TODO: share this utility with browser popup header? */ + protected isScrolled: Signal = toSignal( + this.scrollable + .elementScrolled() + .pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)), + { initialValue: false }, + ); +} diff --git a/libs/components/src/drawer/drawer-close.directive.ts b/libs/components/src/drawer/drawer-close.directive.ts new file mode 100644 index 00000000000..bf56dd8b71f --- /dev/null +++ b/libs/components/src/drawer/drawer-close.directive.ts @@ -0,0 +1,29 @@ +import { Directive, inject } from "@angular/core"; + +import { DrawerComponent } from "./drawer.component"; + +/** + * Closes the ancestor drawer + * + * @example + * + * ```html + * + * + * + * ``` + **/ +@Directive({ + selector: "button[bitDrawerClose]", + standalone: true, + host: { + "(click)": "onClick()", + }, +}) +export class DrawerCloseDirective { + private drawer = inject(DrawerComponent, { optional: true }); + + protected onClick() { + this.drawer?.open.set(false); + } +} diff --git a/libs/components/src/drawer/drawer-header.component.html b/libs/components/src/drawer/drawer-header.component.html new file mode 100644 index 00000000000..4652e5537ee --- /dev/null +++ b/libs/components/src/drawer/drawer-header.component.html @@ -0,0 +1,15 @@ +
+
+ +

+ {{ title() }} +

+
+ +
diff --git a/libs/components/src/drawer/drawer-header.component.ts b/libs/components/src/drawer/drawer-header.component.ts new file mode 100644 index 00000000000..de112a448cf --- /dev/null +++ b/libs/components/src/drawer/drawer-header.component.ts @@ -0,0 +1,35 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostBinding, input } from "@angular/core"; + +import { I18nPipe } from "@bitwarden/ui-common"; + +import { IconButtonModule } from "../icon-button"; +import { TypographyModule } from "../typography"; + +import { DrawerCloseDirective } from "./drawer-close.directive"; + +/** + * Header container for `bit-drawer` + **/ +@Component({ + selector: "bit-drawer-header", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, DrawerCloseDirective, TypographyModule, IconButtonModule, I18nPipe], + templateUrl: "drawer-header.component.html", + host: { + class: "tw-block tw-pl-4 tw-pr-2 tw-py-2", + }, +}) +export class DrawerHeaderComponent { + /** + * The title to display + */ + title = input.required(); + + /** We don't want to set the HTML title attribute with `this.title` */ + @HostBinding("attr.title") + protected get getTitle(): null { + return null; + } +} diff --git a/libs/components/src/drawer/drawer-host.directive.ts b/libs/components/src/drawer/drawer-host.directive.ts new file mode 100644 index 00000000000..f5e3e56b099 --- /dev/null +++ b/libs/components/src/drawer/drawer-host.directive.ts @@ -0,0 +1,28 @@ +import { Portal } from "@angular/cdk/portal"; +import { Directive, signal } from "@angular/core"; + +/** + * Host that renders a drawer + * + * @internal + */ +@Directive({ + selector: "[bitDrawerHost]", + standalone: true, +}) +export class DrawerHostDirective { + private _portal = signal | undefined>(undefined); + + /** The portal to display */ + portal = this._portal.asReadonly(); + + open(portal: Portal) { + this._portal.set(portal); + } + + close(portal: Portal) { + if (portal === this.portal()) { + this._portal.set(undefined); + } + } +} diff --git a/libs/components/src/drawer/drawer.component.html b/libs/components/src/drawer/drawer.component.html new file mode 100644 index 00000000000..fce6b3c57eb --- /dev/null +++ b/libs/components/src/drawer/drawer.component.html @@ -0,0 +1,8 @@ + +
+ +
+
diff --git a/libs/components/src/drawer/drawer.component.ts b/libs/components/src/drawer/drawer.component.ts new file mode 100644 index 00000000000..ccabb6f0b6e --- /dev/null +++ b/libs/components/src/drawer/drawer.component.ts @@ -0,0 +1,76 @@ +import { CdkPortal, PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + input, + model, + viewChild, +} from "@angular/core"; + +import { DrawerHostDirective } from "./drawer-host.directive"; + +/** + * A drawer is a panel of supplementary content that is adjacent to the page's main content. + * + * Drawers render in `bit-layout`. Drawers must be a descendant of `bit-layout`, but they do not need to be a direct descendant. + */ +@Component({ + selector: "bit-drawer", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, PortalModule], + templateUrl: "drawer.component.html", +}) +export class DrawerComponent { + private drawerHost = inject(DrawerHostDirective); + private portal = viewChild.required(CdkPortal); + + /** + * Whether or not the drawer is open. + * + * Note: Does not support implicit boolean transform due to Angular limitation. Must be bound explicitly `[open]="true"` instead of just `open`. + * https://github.com/angular/angular/issues/55166#issuecomment-2032150999 + **/ + open = model(false); + + /** + * The ARIA role of the drawer. + * + * - [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) + * - For drawers that contain content that is complementary to the page's main content. (default) + * - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) + * - For drawers that primary contain links to other content. + */ + role = input<"complementary" | "navigation">("complementary"); + + constructor() { + effect( + () => { + this.open() ? this.drawerHost.open(this.portal()) : this.drawerHost.close(this.portal()); + }, + { + allowSignalWrites: true, + }, + ); + + // Set `open` to `false` when another drawer is opened. + effect( + () => { + if (this.drawerHost.portal() !== this.portal()) { + this.open.set(false); + } + }, + { + allowSignalWrites: true, + }, + ); + } + + /** Toggle the drawer between open & closed */ + toggle() { + this.open.update((prev) => !prev); + } +} diff --git a/libs/components/src/drawer/drawer.mdx b/libs/components/src/drawer/drawer.mdx new file mode 100644 index 00000000000..0098ce64ea9 --- /dev/null +++ b/libs/components/src/drawer/drawer.mdx @@ -0,0 +1,120 @@ +import { Meta, Story, Primary, Controls } from "@storybook/addon-docs"; + +import * as stories from "./drawer.stories"; + +import { DrawerOpen as KitchenSink } from "../stories/kitchen-sink/kitchen-sink.stories"; + + + +```ts +import { DrawerComponent } from "@bitwarden/components"; +``` + +# Drawer + +A drawer is a panel of supplementary content that is adjacent to the page's main content. + + + + + +## Usage + +A `bit-drawer` in a template will not render inline, but rather will render adjacent to the main +page content. + +```html + + + +

Lorem ipsum dolor...

+
+
+``` + +`bit-drawer` must be a descendant of `bit-layout`, but it does not need to be a direct descendant. + +## Header and body + +Header and body content can be provided with the `bit-drawer-header` and `bit-drawer-body` +components, respectively. + +A title can be passed to the header by input: +`` + +Custom content can be rendered before the title with the header's `start` slot: + +```html + + + +``` + +## Opening and closing + +`bit-drawer` opens when its `open` input is `true`: + +```html +... +``` + +Note: Model inputs do not support implicit boolean transformation (see Angular reasoning +[here](https://github.com/angular/angular/issues/55166#issuecomment-2032150999)). `open` must be +bound explicitly `` instead of just ``. + +Buttons can be made to open/toggle drawers by referencing a template variable, or by manipulating +state that is bound to `open`: + +```html + ... +``` + +For convenience, close buttons can be created _inside_ the drawer with the `bitDrawerClose` +directive: + +```html + + + +``` + +## Multiple Drawers + +Only one drawer can be open at a time, and they do not stack. If a drawer is already open, opening +another will close and replace the one already open. + + + +## Headless + +Omitting `bit-drawer-header` and `bit-drawer-body` allows for fully customizable content. + + + +## Accessibility + +- The drawer should contain an h2 element. If you are using `bit-drawer-header`, this is created for + you via the `title` input: + +```html + +

Hello world!

+
+ + + + + + +``` + +- The ARIA role of the drawer can be set with the `role` attribute: + - [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) + (default) + - For drawers that contain content that is complementary to the page's main content. + - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) + - For drawers that primary contain links to other content. + +## Kitchen Sink + + diff --git a/libs/components/src/drawer/drawer.module.ts b/libs/components/src/drawer/drawer.module.ts new file mode 100644 index 00000000000..9f51ba06b4e --- /dev/null +++ b/libs/components/src/drawer/drawer.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { DrawerBodyComponent } from "./drawer-body.component"; +import { DrawerCloseDirective } from "./drawer-close.directive"; +import { DrawerHeaderComponent } from "./drawer-header.component"; +import { DrawerComponent } from "./drawer.component"; + +@NgModule({ + imports: [DrawerComponent, DrawerHeaderComponent, DrawerBodyComponent, DrawerCloseDirective], + exports: [DrawerComponent, DrawerHeaderComponent, DrawerBodyComponent, DrawerCloseDirective], +}) +export class DrawerModule {} diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts new file mode 100644 index 00000000000..54b4c89f4ce --- /dev/null +++ b/libs/components/src/drawer/drawer.stories.ts @@ -0,0 +1,124 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { RouterTestingModule } from "@angular/router/testing"; +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { CalloutModule } from "../callout"; +import { LayoutComponent } from "../layout"; +import { mockLayoutI18n } from "../layout/mocks"; +import { + disableBothThemeDecorator, + positionFixedWrapperDecorator, +} from "../stories/storybook-decorators"; +import { TypographyModule } from "../typography"; +import { I18nMockService } from "../utils"; + +import { DrawerBodyComponent } from "./drawer-body.component"; +import { DrawerHeaderComponent } from "./drawer-header.component"; +import { DrawerComponent } from "./drawer.component"; +import { DrawerModule } from "./drawer.module"; + +export default { + title: "Component Library/Drawer", + component: DrawerComponent, + subcomponents: { + DrawerHeaderComponent, + DrawerBodyComponent, + }, + decorators: [ + positionFixedWrapperDecorator(), + disableBothThemeDecorator, + moduleMetadata({ + imports: [ + RouterTestingModule, + LayoutComponent, + DrawerModule, + ButtonModule, + CalloutModule, + TypographyModule, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + ...mockLayoutI18n, + close: "Close", + }); + }, + }, + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

The drawer is {{ open ? "open" : "closed" }}.

+ + + + + + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +

+ +
+ + `, + }), + args: { + open: true, + }, +}; + +export const Headless: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

The drawer is {{ open ? "open" : "closed" }}.

+ + +

+ Hello world! +
+ + `, + }), + args: { + open: true, + }, +}; + +export const MultipleDrawers: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + Foo + + + + Bar + + + `, + }), +}; diff --git a/libs/components/src/drawer/index.ts b/libs/components/src/drawer/index.ts new file mode 100644 index 00000000000..abf5b8d34f1 --- /dev/null +++ b/libs/components/src/drawer/index.ts @@ -0,0 +1,5 @@ +export * from "./drawer.module"; +export * from "./drawer.component"; +export * from "./drawer-body.component"; +export * from "./drawer-close.directive"; +export * from "./drawer-header.component"; diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index 8abd0786276..690c00a9dc0 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -5,8 +5,8 @@ import { NgClass } from "@angular/common"; import { Component, ContentChild, HostBinding, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "@bitwarden/ui-common"; -import { I18nPipe } from "../shared/i18n.pipe"; import { TypographyDirective } from "../typography/typography.directive"; import { BitFormControlAbstraction } from "./form-control.abstraction"; diff --git a/libs/components/src/form-field/bit-validators.stories.ts b/libs/components/src/form-field/bit-validators.stories.ts index 642ff30bb5a..748f92f2523 100644 --- a/libs/components/src/form-field/bit-validators.stories.ts +++ b/libs/components/src/form-field/bit-validators.stories.ts @@ -35,7 +35,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index ad550b49260..1709c3078fa 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -4,7 +4,7 @@ import { Component, Input } from "@angular/core"; import { AbstractControl, UntypedFormGroup } from "@angular/forms"; -import { I18nPipe } from "../shared/i18n.pipe"; +import { I18nPipe } from "@bitwarden/ui-common"; @Component({ selector: "bit-error-summary", diff --git a/libs/components/src/form-field/error-summary.stories.ts b/libs/components/src/form-field/error-summary.stories.ts index 4e1031abaf6..4b1a30c45b3 100644 --- a/libs/components/src/form-field/error-summary.stories.ts +++ b/libs/components/src/form-field/error-summary.stories.ts @@ -34,7 +34,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 9f41c6cf6ac..4f7c2a67483 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -14,10 +14,11 @@ import { signal, } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitHintComponent } from "../form-control/hint.component"; import { BitLabel } from "../form-control/label.component"; import { inputBorderClasses } from "../input/input.directive"; -import { I18nPipe } from "../shared/i18n.pipe"; import { BitErrorComponent } from "./error.component"; import { BitFormFieldControl } from "./form-field-control"; diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index ccd80d6fa75..738ac96bf76 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -1,4 +1,7 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { TextFieldModule } from "@angular/cdk/text-field"; +import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; import { AbstractControl, UntypedFormBuilder, @@ -10,7 +13,6 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { A11yTitleDirective } from "@bitwarden/angular/src/directives/a11y-title.directive"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AsyncActionsModule } from "../async-actions"; @@ -29,6 +31,41 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { BitFormFieldComponent } from "./form-field.component"; import { FormFieldModule } from "./form-field.module"; +// TOOD: This solves a circular dependency between components and angular. +@Directive({ + selector: "[appA11yTitle]", +}) +export class A11yTitleDirective implements OnInit { + @Input() set appA11yTitle(title: string) { + this.title = title; + this.setAttributes(); + } + + private title: string; + private originalTitle: string | null; + private originalAriaLabel: string | null; + + constructor( + private el: ElementRef, + private renderer: Renderer2, + ) {} + + ngOnInit() { + this.originalTitle = this.el.nativeElement.getAttribute("title"); + this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label"); + this.setAttributes(); + } + + private setAttributes() { + if (this.originalTitle === null) { + this.renderer.setAttribute(this.el.nativeElement, "title", this.title); + } + if (this.originalAriaLabel === null) { + this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title); + } + } +} + export default { title: "Component Library/Form/Field", component: BitFormFieldComponent, @@ -71,7 +108,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/multi-select.stories.ts b/libs/components/src/form-field/multi-select.stories.ts index da4776ab025..8d84aa735bf 100644 --- a/libs/components/src/form-field/multi-select.stories.ts +++ b/libs/components/src/form-field/multi-select.stories.ts @@ -56,7 +56,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5600%3A24278", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/password-input-toggle.stories.ts b/libs/components/src/form-field/password-input-toggle.stories.ts index 094f939e0ea..d46ec92ab37 100644 --- a/libs/components/src/form-field/password-input-toggle.stories.ts +++ b/libs/components/src/form-field/password-input-toggle.stories.ts @@ -27,7 +27,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, docs: { description: { diff --git a/libs/components/src/form/form.stories.ts b/libs/components/src/form/form.stories.ts index 23b2cc8cea2..6aef140fe5f 100644 --- a/libs/components/src/form/form.stories.ts +++ b/libs/components/src/form/form.stories.ts @@ -64,7 +64,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 6274bb2b8d1..08c95c5d641 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4369%3A16686", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-37011&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 54cdd7928cd..53454567b7f 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -6,6 +6,12 @@ import * as GenericIcons from "./icons"; export default { title: "Component Library/Icon", component: BitIconComponent, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-50335&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index a48750a99ff..7788f4986bf 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,3 +1,5 @@ +export { ButtonType } from "./shared/button-like.abstraction"; +export * from "./a11y"; export * from "./async-actions"; export * from "./avatar"; export * from "./badge-list"; @@ -5,15 +7,16 @@ export * from "./badge"; export * from "./banner"; export * from "./breadcrumbs"; export * from "./button"; -export { ButtonType } from "./shared/button-like.abstraction"; export * from "./callout"; export * from "./card"; export * from "./checkbox"; export * from "./chip-select"; export * from "./color-password"; export * from "./container"; +export * from "./copy-click"; export * from "./dialog"; export * from "./disclosure"; +export * from "./drawer"; export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index 5adf9d3c49d..3a64a334d0a 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -53,6 +53,12 @@ export default { }), componentWrapperDecorator((story) => `
${story}
`), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-37011&t=b5tDKylm5sWm2yKo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index db889d3ae13..0df66bf7256 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -42,4 +42,5 @@
} +
diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index d55ad8493eb..7bf8a6ad173 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -1,7 +1,9 @@ +import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; +import { DrawerHostDirective } from "../drawer/drawer-host.directive"; import { LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -10,12 +12,14 @@ import { SharedModule } from "../shared"; selector: "bit-layout", templateUrl: "layout.component.html", standalone: true, - imports: [CommonModule, SharedModule, LinkModule, RouterModule], + imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule], + hostDirectives: [DrawerHostDirective], }) export class LayoutComponent { protected mainContentId = "main-content"; - constructor(protected sideNavService: SideNavService) {} + protected sideNavService = inject(SideNavService); + protected drawerPortal = inject(DrawerHostDirective).portal; focusMainContent() { document.getElementById(this.mainContentId)?.focus(); diff --git a/libs/components/src/layout/layout.stories.ts b/libs/components/src/layout/layout.stories.ts index a0eadebe7fa..e09055df596 100644 --- a/libs/components/src/layout/layout.stories.ts +++ b/libs/components/src/layout/layout.stories.ts @@ -6,10 +6,11 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { CalloutModule } from "../callout"; import { NavigationModule } from "../navigation"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { LayoutComponent } from "./layout.component"; +import { mockLayoutI18n } from "./mocks"; export default { title: "Component Library/Layout", @@ -22,12 +23,7 @@ export default { { provide: I18nService, useFactory: () => { - return new I18nMockService({ - toggleSideNavigation: "Toggle side navigation", - skipToContent: "Skip to content", - submenu: "submenu", - toggleCollapse: "toggle collapse", - }); + return new I18nMockService(mockLayoutI18n); }, }, ], @@ -35,6 +31,10 @@ export default { ], parameters: { chromatic: { viewports: [640, 1280] }, + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-51009&t=k6OTDDPZOTtypRqo-11", + }, }, } as Meta; diff --git a/libs/components/src/layout/mocks.ts b/libs/components/src/layout/mocks.ts new file mode 100644 index 00000000000..50c2bd9afb2 --- /dev/null +++ b/libs/components/src/layout/mocks.ts @@ -0,0 +1,7 @@ +/** TODO: create mock messages.json file for all of CL in favor of sharing per-Story mocks */ +export const mockLayoutI18n = { + toggleSideNavigation: "Toggle side navigation", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", +}; diff --git a/libs/components/src/link/link.stories.ts b/libs/components/src/link/link.stories.ts index cc3d26dc9d2..d07d33ae589 100644 --- a/libs/components/src/link/link.stories.ts +++ b/libs/components/src/link/link.stories.ts @@ -19,7 +19,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17419", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-39582&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index 65fafd2d04d..f1f4d8df000 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -17,7 +17,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17952", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40144&t=b5tDKylm5sWm2yKo-11", }, }, } as Meta; diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index 7c18b3c13d6..cd92eb1d7ae 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -23,10 +23,10 @@ import { import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "@bitwarden/ui-common"; import { BadgeModule } from "../badge"; import { BitFormFieldControl } from "../form-field/form-field-control"; -import { I18nPipe } from "../shared/i18n.pipe"; import { SelectItemView } from "./models/select-item-view"; diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index b09eeb94b2e..62bdee26740 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -12,8 +12,9 @@ import { SkipSelf, } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; import { NavBaseComponent } from "./nav-base.component"; import { NavGroupAbstraction, NavItemComponent } from "./nav-item.component"; @@ -71,6 +72,8 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI setOpen(isOpen: boolean) { this.open = isOpen; this.openChange.emit(this.open); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.open && this.parentNavGroup?.setOpen(this.open); } diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts index a6fa53ff187..6fbb89d4d9e 100644 --- a/libs/components/src/navigation/nav-group.stories.ts +++ b/libs/components/src/navigation/nav-group.stories.ts @@ -6,8 +6,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LayoutComponent } from "../layout"; import { SharedModule } from "../shared/shared.module"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { NavGroupComponent } from "./nav-group.component"; import { NavigationModule } from "./navigation.module"; @@ -62,7 +62,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40145&t=b5tDKylm5sWm2yKo-4", }, chromatic: { viewports: [640, 1280] }, }, diff --git a/libs/components/src/navigation/nav-item.stories.ts b/libs/components/src/navigation/nav-item.stories.ts index 20d6ebd1d7e..0f6f406b2eb 100644 --- a/libs/components/src/navigation/nav-item.stories.ts +++ b/libs/components/src/navigation/nav-item.stories.ts @@ -5,8 +5,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { IconButtonModule } from "../icon-button"; import { LayoutComponent } from "../layout"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { NavItemComponent } from "./nav-item.component"; import { NavigationModule } from "./navigation.module"; @@ -39,7 +39,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40145&t=b5tDKylm5sWm2yKo-4", }, chromatic: { viewports: [640, 1280] }, }, diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts index c86a517100f..e8e4f131d6d 100644 --- a/libs/components/src/navigation/side-nav.component.ts +++ b/libs/components/src/navigation/side-nav.component.ts @@ -4,8 +4,9 @@ import { CdkTrapFocus } from "@angular/cdk/a11y"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Input, ViewChild } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitIconButtonComponent } from "../icon-button/icon-button.component"; -import { I18nPipe } from "../shared/i18n.pipe"; import { NavDividerComponent } from "./nav-divider.component"; import { SideNavService } from "./side-nav.service"; diff --git a/libs/components/src/no-items/no-items.stories.ts b/libs/components/src/no-items/no-items.stories.ts index d8e5b59bdbf..48d52476b17 100644 --- a/libs/components/src/no-items/no-items.stories.ts +++ b/libs/components/src/no-items/no-items.stories.ts @@ -13,6 +13,12 @@ export default { imports: [ButtonModule, NoItemsModule], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21665-25102&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/popover/popover.stories.ts b/libs/components/src/popover/popover.stories.ts index 10b7b248b79..fca4d659607 100644 --- a/libs/components/src/popover/popover.stories.ts +++ b/libs/components/src/popover/popover.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1717-15868", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40852&t=b5tDKylm5sWm2yKo-4", }, }, argTypes: { diff --git a/libs/components/src/progress/progress.stories.ts b/libs/components/src/progress/progress.stories.ts index 49a5398d2d9..1484dab0a21 100644 --- a/libs/components/src/progress/progress.stories.ts +++ b/libs/components/src/progress/progress.stories.ts @@ -8,7 +8,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A18185&t=AM0acaIJ00BUhZKz-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40933&t=b5tDKylm5sWm2yKo-4", }, }, args: { diff --git a/libs/components/src/radio-button/radio-button.stories.ts b/libs/components/src/radio-button/radio-button.stories.ts index dcef13a19be..f5d7f6732f5 100644 --- a/libs/components/src/radio-button/radio-button.stories.ts +++ b/libs/components/src/radio-button/radio-button.stories.ts @@ -31,7 +31,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-35836&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index b22bd7ef377..895f769af50 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -4,8 +4,9 @@ import { NgTemplateOutlet } from "@angular/common"; import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core"; import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitLabel } from "../form-control/label.component"; -import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index 9a811ce6777..7f1bd781e9d 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -9,10 +9,10 @@ import { } from "@angular/forms"; import { isBrowserSafariApi } from "@bitwarden/platform"; +import { I18nPipe } from "@bitwarden/ui-common"; import { InputModule } from "../input/input.module"; import { FocusableElement } from "../shared/focusable-element"; -import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; diff --git a/libs/components/src/section/section-header.component.html b/libs/components/src/section/section-header.component.html index 3f96e22540f..f070cfeae02 100644 --- a/libs/components/src/section/section-header.component.html +++ b/libs/components/src/section/section-header.component.html @@ -2,7 +2,7 @@
-
+
diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index 0d36d6e5a11..53e6bc078c5 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -22,6 +22,12 @@ export default { }), componentWrapperDecorator((story) => `
${story}
`), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21666-19363&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/select/select.stories.ts b/libs/components/src/select/select.stories.ts index 4bc85d8dba2..c030bea86c5 100644 --- a/libs/components/src/select/select.stories.ts +++ b/libs/components/src/select/select.stories.ts @@ -33,7 +33,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/3tWtMSYoLB0ZLEimLNzYsm/End-user-%26-admin-Vault-Refresh?t=7QEmGA69YTOF8sXU-0", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/shared/shared.module.ts b/libs/components/src/shared/shared.module.ts index 253b049f8fe..99d052c3350 100644 --- a/libs/components/src/shared/shared.module.ts +++ b/libs/components/src/shared/shared.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { I18nPipe } from "./i18n.pipe"; +import { I18nPipe } from "@bitwarden/ui-common"; @NgModule({ imports: [CommonModule, I18nPipe], diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts index 02b49a3e915..7709506f050 100644 --- a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -12,7 +12,7 @@ import { TableDataSource, TableModule } from "../../../table"; imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule], template: /*html*/ ` - + Id diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index d0c31b06b39..9c609300ed1 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -1,5 +1,5 @@ import { DialogRef } from "@angular/cdk/dialog"; -import { Component } from "@angular/core"; +import { Component, signal } from "@angular/core"; import { DialogService } from "../../../dialog"; import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; @@ -15,8 +15,8 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; Dialog body text goes here. - - + + `, @@ -28,13 +28,7 @@ class KitchenSinkDialog { @Component({ standalone: true, selector: "bit-tab-main", - imports: [ - KitchenSinkSharedModule, - KitchenSinkTable, - KitchenSinkToggleList, - KitchenSinkForm, - KitchenSinkDialog, - ], + imports: [KitchenSinkSharedModule, KitchenSinkTable, KitchenSinkToggleList, KitchenSinkForm], template: ` Kitchen Sink test zone @@ -50,6 +44,11 @@ class KitchenSinkDialog {

+
+

Bitwarden Kitchen Sink

+ Learn more +
+

The purpose of this story is to compose together all of our components. When snapshot tests @@ -65,18 +64,14 @@ class KitchenSinkDialog {

-
-

Bitwarden

- Learn more -
-

About

- + +

Companies using Bitwarden

@@ -101,15 +96,87 @@ class KitchenSinkDialog {
+ + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+ + What did foo say to bar? + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+
+
`, }) export class KitchenSinkMainComponent { constructor(public dialogService: DialogService) {} - openDefaultDialog() { + protected drawerOpen = signal(false); + + openDialog() { this.dialogService.open(KitchenSinkDialog); } + openDrawer() { + this.drawerOpen.set(true); + } + navItems = [ { icon: "bwi-collection", name: "Password Managers", route: "/" }, { icon: "bwi-collection", name: "Favorites", route: "/" }, diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts index 3c6d6f11444..ba71483d7de 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts @@ -20,7 +20,11 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Password Manager Everyone - + Anchor link Another link @@ -33,7 +37,11 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Secrets Manager Developers - + Anchor link Another link diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts index c5a487aaf0d..c71140d8166 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -7,7 +7,7 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; selector: "bit-kitchen-sink-toggle-list", imports: [KitchenSinkSharedModule], template: ` -
+
All 3 diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts index 56e3a92e2a3..c4fe2f9b2af 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts @@ -13,6 +13,7 @@ import { CalloutModule } from "../../callout"; import { CheckboxModule } from "../../checkbox"; import { ColorPasswordModule } from "../../color-password"; import { DialogModule } from "../../dialog"; +import { DrawerModule } from "../../drawer"; import { FormControlModule } from "../../form-control"; import { FormFieldModule } from "../../form-field"; import { IconModule } from "../../icon"; @@ -48,6 +49,7 @@ import { TypographyModule } from "../../typography"; ColorPasswordModule, CommonModule, DialogModule, + DrawerModule, FormControlModule, FormFieldModule, FormsModule, @@ -85,6 +87,7 @@ import { TypographyModule } from "../../typography"; ColorPasswordModule, CommonModule, DialogModule, + DrawerModule, FormControlModule, FormFieldModule, FormsModule, diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 44080e29049..62b93984384 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -1,13 +1,7 @@ import { importProvidersFrom } from "@angular/core"; import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; -import { - Meta, - StoryObj, - applicationConfig, - componentWrapperDecorator, - moduleMetadata, -} from "@storybook/angular"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; import { userEvent, getAllByRole, @@ -23,6 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; +import { disableBothThemeDecorator, positionFixedWrapperDecorator } from "../storybook-decorators"; import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component"; import { KitchenSinkForm } from "./components/kitchen-sink-form.component"; @@ -35,25 +30,8 @@ export default { title: "Documentation / Kitchen Sink", component: LayoutComponent, decorators: [ - componentWrapperDecorator( - /** - * Applying a CSS transform makes a `position: fixed` element act like it is `position: relative` - * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 - */ - (story) => { - return /* HTML */ `
- ${story} -
`; - }, - ({ globals }) => { - /** - * avoid a bug with the way that we render the same component twice in the same iframe and how - * that interacts with the router-outlet - */ - const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; - return { theme: themeOverride }; - }, - ), + positionFixedWrapperDecorator(), + disableBothThemeDecorator, moduleMetadata({ imports: [ KitchenSinkSharedModule, @@ -135,7 +113,7 @@ export const MenuOpen: Story = { }, }; -export const DefaultDialogOpen: Story = { +export const DialogOpen: Story = { ...Default, play: async (context) => { const canvas = context.canvasElement; @@ -148,6 +126,19 @@ export const DefaultDialogOpen: Story = { }, }; +export const DrawerOpen: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const drawerButton = getByRole(canvas, "button", { + name: "Open Drawer", + }); + + // workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075 + await fireEvent.click(drawerButton); + }, +}; + export const PopoverOpen: Story = { ...Default, play: async (context) => { @@ -196,4 +187,10 @@ export const VirtualScrollBlockingDialog: Story = { await userEvent.click(dialogButton); }, + parameters: { + chromatic: { + // TODO CL-524 fix flaky story (number of virtual scroll rows is inconsistent) + disableSnapshot: true, + }, + }, }; diff --git a/libs/components/src/utils/position-fixed-wrapper-decorator.ts b/libs/components/src/stories/storybook-decorators.ts similarity index 50% rename from libs/components/src/utils/position-fixed-wrapper-decorator.ts rename to libs/components/src/stories/storybook-decorators.ts index a3298e6ad03..d59f2dd1f3e 100644 --- a/libs/components/src/utils/position-fixed-wrapper-decorator.ts +++ b/libs/components/src/stories/storybook-decorators.ts @@ -11,7 +11,21 @@ export const positionFixedWrapperDecorator = (wrapper?: (story: string) => strin * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 */ (story) => - /* HTML */ `
+ /* HTML */ `
${wrapper ? wrapper(story) : story}
`, ); + +export const disableBothThemeDecorator = componentWrapperDecorator( + (story) => story, + ({ globals }) => { + /** + * avoid a bug with the way that we render the same component twice in the same iframe and how + * that interacts with the router-outlet + */ + const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; + return { theme: themeOverride }; + }, +); diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index d3309c03aa9..bdfb87ac52f 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -10,7 +10,12 @@ import { TableComponent } from "./table.component"; @Component({ selector: "th[bitSortable]", template: ` - diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index 4ebc3045d13..e8ab24ee8b7 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -21,7 +21,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A18371", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41282&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index 87435133a23..2a71a385a83 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -44,7 +44,7 @@ export class TabListItemDirective implements FocusableOption { */ get textColorClassList(): string[] { if (this.disabled) { - return ["!tw-text-muted", "hover:!tw-text-muted"]; + return ["!tw-text-secondary-300", "hover:!tw-text-secondary-300"]; } if (this.active) { return ["!tw-text-primary-600", "hover:!tw-text-primary-700"]; @@ -60,7 +60,7 @@ export class TabListItemDirective implements FocusableOption { "tw-px-4", "tw-font-semibold", "tw-transition", - "tw-rounded-t", + "tw-rounded-t-lg", "tw-border-0", "tw-border-x", "tw-border-t-4", @@ -71,12 +71,12 @@ export class TabListItemDirective implements FocusableOption { "focus-visible:tw-z-10", "focus-visible:tw-outline-none", "focus-visible:tw-ring-2", - "focus-visible:tw-ring-primary-700", + "focus-visible:tw-ring-primary-600", ]; } get disabledClassList(): string[] { - return ["!tw-bg-secondary-100", "!tw-no-underline", "tw-cursor-not-allowed"]; + return ["!tw-no-underline", "tw-cursor-not-allowed"]; } get activeClassList(): string[] { @@ -87,6 +87,7 @@ export class TabListItemDirective implements FocusableOption { "tw-border-b", "tw-border-b-background", "!tw-bg-background", + "hover:tw-no-underline", "hover:tw-border-t-primary-700", "focus-visible:tw-border-t-primary-700", "focus-visible:!tw-text-primary-700", diff --git a/libs/components/src/tabs/tabs.stories.ts b/libs/components/src/tabs/tabs.stories.ts index 6b460d8ee00..250a7443065 100644 --- a/libs/components/src/tabs/tabs.stories.ts +++ b/libs/components/src/tabs/tabs.stories.ts @@ -74,7 +74,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17922", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41432&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts index f0b55402194..5afed111c0f 100644 --- a/libs/components/src/toast/index.ts +++ b/libs/components/src/toast/index.ts @@ -1,2 +1,3 @@ export * from "./toast.module"; export * from "./toast.service"; +export type { ToastVariant } from "./toast.component"; diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts index 382e19097b0..abb737f5c23 100644 --- a/libs/components/src/toast/toast.stories.ts +++ b/libs/components/src/toast/toast.stories.ts @@ -63,7 +63,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41506&t=b5tDKylm5sWm2yKo-11", }, }, } as Meta; diff --git a/libs/components/src/toggle-group/toggle-group.stories.ts b/libs/components/src/toggle-group/toggle-group.stories.ts index fc8ea0ea929..4860636c159 100644 --- a/libs/components/src/toggle-group/toggle-group.stories.ts +++ b/libs/components/src/toggle-group/toggle-group.stories.ts @@ -19,7 +19,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17157", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881-17157&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 26616d07156..6e887030c34 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -11,6 +11,7 @@ module.exports = { content: [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/key-management/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", ], safelist: [], diff --git a/libs/components/tailwind.config.js b/libs/components/tailwind.config.js index 7a53c82ec58..2df648723f0 100644 --- a/libs/components/tailwind.config.js +++ b/libs/components/tailwind.config.js @@ -4,6 +4,7 @@ const config = require("./tailwind.config.base"); config.content = [ "libs/components/src/**/*.{html,ts,mdx}", "libs/auth/src/**/*.{html,ts,mdx}", + "libs/vault/src/**/*.{html,ts,mdx}", "apps/web/src/**/*.{html,ts,mdx}", "apps/browser/src/**/*.{html,ts,mdx}", "bitwarden_license/bit-web/src/**/*.{html,ts,mdx}", diff --git a/libs/components/tsconfig.json b/libs/components/tsconfig.json index dabcecf78e9..abd5830d425 100644 --- a/libs/components/tsconfig.json +++ b/libs/components/tsconfig.json @@ -20,8 +20,8 @@ "lib": ["es2020", "dom"], "paths": { "@bitwarden/common/*": ["../common/src/*"], - "@bitwarden/angular/*": ["../angular/src/*"], - "@bitwarden/platform": ["../platform/src"] + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"] }, "plugins": [ { diff --git a/libs/importer/jest.config.js b/libs/importer/jest.config.js index 68daba3d407..ab449dc7757 100644 --- a/libs/importer/jest.config.js +++ b/libs/importer/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../shared/jest.config.ts"); diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 0da8127369e..c53a1d3f522 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -395,6 +395,11 @@

{{ "data" | i18n }}

Open the FullClient, go to the Main Menu and select Export. Start the export passwords wizard and follow the instructions to export a CSV file. + + Log in to NordPass and open Settings → Scroll down to the Import and Export section + and select Export items → Enter your Master Password and select Continue. → Save + the CSV file on your device. + {{ "data" | i18n }}
{{ "selectImportFile" | i18n }} -
+
diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index f2bf7471d44..d574a6fc094 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -23,11 +23,15 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -138,8 +142,14 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { @Input() set organizationId(value: string) { this._organizationId = value; - this.organizationService - .get$(this._organizationId) + getUserId(this.accountService.activeAccount$) + .pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this._organizationId)), + ), + ) .pipe(takeUntil(this.destroy$)) .subscribe((organization) => { this._organizationId = organization?.id; @@ -294,8 +304,9 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { } private async initializeOrganizations() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizations$ = concat( - this.organizationService.memberOrganizations$.pipe( + this.organizationService.memberOrganizations$(userId).pipe( // Import is an alternative way to create collections during onboarding, so import from Password Manager // is available to any user who can create collections in the organization. map((orgs) => orgs.filter((org) => org.canAccessImport || org.canCreateNewCollections)), @@ -408,7 +419,14 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { if (!organizationId) { return false; } - return (await this.organizationService.get(this.organizationId))?.canAccessImport; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + return ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ) + )?.canAccessImport; } getFormatInstructionTitle() { diff --git a/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts b/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts index 368a9b321be..62826ae3fe9 100644 --- a/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts +++ b/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts @@ -12,9 +12,9 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DialogService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { DialogService } from "../../../../components/src/dialog"; import { ClientInfo, Vault } from "../../importers/lastpass/access"; import { FederatedUserContext } from "../../importers/lastpass/access/models"; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 8b497beac93..e8f63ef8705 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -267,7 +267,7 @@ describe("ImportService", () => { function createCipher(options: Partial = {}) { const cipher = new CipherView(); - cipher.name; + cipher.name = options.name; cipher.type = options.type; cipher.folderId = options.folderId; cipher.collectionIds = options.collectionIds; diff --git a/libs/importer/tsconfig.json b/libs/importer/tsconfig.json index 6004a56fb55..09eb33e2884 100644 --- a/libs/importer/tsconfig.json +++ b/libs/importer/tsconfig.json @@ -1,5 +1,22 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/key-management/jest.config.js b/libs/key-management/jest.config.js index e20d02303d9..ad8023e906b 100644 --- a/libs/key-management/jest.config.js +++ b/libs/key-management/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/key-management/package.json b/libs/key-management/package.json index 083386cbc81..1063e16580e 100644 --- a/libs/key-management/package.json +++ b/libs/key-management/package.json @@ -15,7 +15,8 @@ "scripts": { "clean": "rimraf dist", "build": "npm run clean && tsc", - "build:watch": "npm run clean && tsc -watch" + "build:watch": "npm run clean && tsc -watch", + "test": "jest" }, "dependencies": { "@bitwarden/angular": "file:../angular", diff --git a/libs/key-management/src/abstractions/kdf-config.service.ts b/libs/key-management/src/abstractions/kdf-config.service.ts index d5f613828d4..9cc39561aa8 100644 --- a/libs/key-management/src/abstractions/kdf-config.service.ts +++ b/libs/key-management/src/abstractions/kdf-config.service.ts @@ -7,5 +7,5 @@ import { KdfConfig } from "../models/kdf-config"; export abstract class KdfConfigService { abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise; abstract getKdfConfig(): Promise; - abstract getKdfConfig$(userId: UserId): Observable; + abstract getKdfConfig$(userId: UserId): Observable; } diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index f5ab624bb22..da59c2986b2 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -5,13 +5,29 @@ import { Observable } from "rxjs"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; import { KdfConfig } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-organization.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileProviderOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-provider-organization.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileProviderResponse } from "../../../common/src/admin-console/models/response/profile-provider.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeySuffixOptions, HashPurpose } from "../../../common/src/platform/enums"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SymmetricCryptoKey } from "../../../common/src/platform/models/domain/symmetric-crypto-key"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { OrganizationId, UserId } from "../../../common/src/types/guid"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserKey, MasterKey, diff --git a/libs/key-management/src/angular/lock/components/lock.component.html b/libs/key-management/src/angular/lock/components/lock.component.html index 7d9ed6124f6..437e29447e2 100644 --- a/libs/key-management/src/angular/lock/components/lock.component.html +++ b/libs/key-management/src/angular/lock/components/lock.component.html @@ -6,15 +6,13 @@ - + + + diff --git a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts index cec818b1cd6..7bcffd92399 100644 --- a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -34,6 +35,7 @@ export class CredentialGeneratorHistoryDialogComponent { private accountService: AccountService, private history: GeneratorHistoryService, private dialogService: DialogService, + private dialogRef: DialogRef, ) { this.accountService.activeAccount$ .pipe( @@ -52,7 +54,13 @@ export class CredentialGeneratorHistoryDialogComponent { .subscribe(this.hasHistory$); } - clear = async () => { + /** closes the dialog */ + protected close() { + this.dialogRef.close(); + } + + /** Launches clear history flow */ + protected async clear() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "clearGeneratorHistoryTitle" }, content: { key: "cleargGeneratorHistoryDescription" }, @@ -64,5 +72,5 @@ export class CredentialGeneratorHistoryDialogComponent { if (confirmed) { await this.history.clear(await firstValueFrom(this.userId$)); } - }; + } } diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index 69ed0b0336d..7e476564de6 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -72,6 +72,6 @@ export class CredentialGeneratorHistoryComponent { protected getGeneratedValueText(credential: GeneratedCredential) { const info = this.generatorService.algorithm(credential.category); - return info.generatedValue; + return info.credentialType; } } diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index ce86abe80ae..624c5ab5860 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -20,7 +20,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > @@ -56,7 +56,7 @@

{{ "options" | i18n }}

- + {{ "type" | i18n }} {{ "options" | i18n }} }} -
+ {{ "service" | i18n }} { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -199,8 +201,12 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + + this.generatedCredential$.next(generated); this.onGenerated.next(generated); - this.value$.next(generated.credential); }); }); @@ -384,7 +390,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { if (!a || a.onlyOnRequest) { - this.value$.next("-"); + this.generatedCredential$.next(null); } else { this.generate("autogenerate").catch((e: unknown) => this.logService.error(e)); } @@ -392,6 +398,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { }); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, @@ -474,7 +484,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); /** Emits hint key for the currently selected credential type */ @@ -483,21 +493,28 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { /** tracks the currently selected credential category */ protected category$ = new ReplaySubject(1); + private readonly generatedCredential$ = new BehaviorSubject(null); + /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); + protected readonly value$ = this.generatedCredential$.pipe( + map((generated) => generated?.credential ?? "-"), + ); /** Emits when the userId changes */ protected readonly userId$ = new BehaviorSubject(null); + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; + /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } private toOptions(algorithms: AlgorithmInfo[]) { @@ -516,7 +533,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // finalize subjects this.generate$.complete(); - this.value$.complete(); + this.generatedCredential$.complete(); // finalize component bindings this.onGenerated.complete(); diff --git a/libs/tools/generator/components/src/forwarder-settings.component.html b/libs/tools/generator/components/src/forwarder-settings.component.html index d610f53d59f..8ad6eab1acf 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.html +++ b/libs/tools/generator/components/src/forwarder-settings.component.html @@ -1,4 +1,4 @@ - + {{ "forwarderDomainName" | i18n }}
{{ "options" | i18n }}
- +
diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index a6aa5ebdd02..c7fa93dc535 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -18,7 +18,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index 7e230d8bfd6..b59b162e687 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { @@ -16,11 +17,9 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { Option } from "@bitwarden/components/src/select/option"; +import { ToastService, Option } from "@bitwarden/components"; import { CredentialGeneratorService, Generators, @@ -29,6 +28,7 @@ import { isPasswordAlgorithm, AlgorithmInfo, isSameAlgorithm, + GenerateRequest, } from "@bitwarden/generator-core"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; @@ -43,9 +43,9 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { private generatorHistoryService: GeneratorHistoryService, private toastService: ToastService, private logService: LogService, - private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. @@ -68,14 +68,17 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { protected readonly userId$ = new BehaviorSubject(null); /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); + + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } /** Tracks changes to the selected credential type @@ -94,6 +97,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); + /** emits algorithm info when the selected algorithm changes */ + @Output() + readonly onAlgorithm = new EventEmitter(); + async ngOnInit() { if (this.userId) { this.userId$.next(this.userId); @@ -134,10 +141,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -147,6 +154,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + this.onGenerated.next(generated); this.value$.next(generated.credential); }); @@ -186,6 +197,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // template bindings refresh immediately this.zone.run(() => { this.algorithm$.next(algorithm); + this.onAlgorithm.next(algorithm); }); }); @@ -201,6 +213,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { }); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, @@ -245,7 +261,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); private toOptions(algorithms: AlgorithmInfo[]) { diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index 9f8e00921fb..5e4d1079725 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -2,7 +2,7 @@

{{ "options" | i18n }}

- +
diff --git a/libs/tools/generator/components/src/subaddress-settings.component.html b/libs/tools/generator/components/src/subaddress-settings.component.html index 1dfb5e3460d..b7f71b12b2a 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.html +++ b/libs/tools/generator/components/src/subaddress-settings.component.html @@ -1,4 +1,4 @@ - + {{ "email" | i18n }} @@ -33,7 +33,7 @@

{{ "options" | i18n }}

- + {{ "type" | i18n }} {{ "options" | i18n }} }} -
+ {{ "service" | i18n }} (); + /** emits algorithm info when the selected algorithm changes */ + @Output() + readonly onAlgorithm = new EventEmitter(); + /** Removes bottom margin from internal elements */ @Input({ transform: coerceBooleanProperty }) disableMargin = false; @@ -157,10 +163,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -170,6 +176,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + this.onGenerated.next(generated); this.value$.next(generated.credential); }); @@ -248,6 +258,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // template bindings refresh immediately this.zone.run(() => { this.algorithm$.next(algorithm); + this.onAlgorithm.next(algorithm); }); }); @@ -356,6 +367,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { throw new Error(`Invalid generator type: "${type}"`); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + /** Lists the credential types supported by the component. */ protected typeOptions$ = new BehaviorSubject[]>([]); @@ -371,6 +386,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { /** tracks the currently selected credential type */ protected algorithm$ = new ReplaySubject(1); + /** Emits hint key for the currently selected credential type */ + protected credentialTypeHint$ = new ReplaySubject(1); + + /** Emits the last generated value. */ + protected readonly value$ = new BehaviorSubject(""); + + /** Emits when the userId changes */ + protected readonly userId$ = new BehaviorSubject(null); + + /** Emits when a new credential is requested */ + private readonly generate$ = new Subject(); + protected showAlgorithm$ = this.algorithm$.pipe( combineLatestWith(this.showForwarder$), map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), @@ -397,27 +424,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); - /** Emits hint key for the currently selected credential type */ - protected credentialTypeHint$ = new ReplaySubject(1); - - /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); - - /** Emits when the userId changes */ - protected readonly userId$ = new BehaviorSubject(null); - - /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } private toOptions(algorithms: AlgorithmInfo[]) { diff --git a/libs/tools/generator/components/src/username-settings.component.html b/libs/tools/generator/components/src/username-settings.component.html index 649cd052e7b..fd8b32e2b3e 100644 --- a/libs/tools/generator/components/src/username-settings.component.html +++ b/libs/tools/generator/components/src/username-settings.component.html @@ -1,4 +1,4 @@ - + ; generate( - request: GenerationRequest, + request: GenerateRequest, settings: SubaddressGenerationOptions, ): Promise; async generate( - _request: GenerationRequest, + request: GenerateRequest, settings: CatchallGenerationOptions | SubaddressGenerationOptions, ) { if (isCatchallGenerationOptions(settings)) { const email = await this.randomAsciiCatchall(settings.catchallDomain); - return new GeneratedCredential(email, "catchall", Date.now()); + return new GeneratedCredential( + email, + "catchall", + Date.now(), + request.source, + request.website, + ); } else if (isSubaddressGenerationOptions(settings)) { const email = await this.randomAsciiSubaddress(settings.subaddressEmail); - return new GeneratedCredential(email, "subaddress", Date.now()); + return new GeneratedCredential( + email, + "subaddress", + Date.now(), + request.source, + request.website, + ); } throw new Error("Invalid settings received by generator."); diff --git a/libs/tools/generator/core/src/engine/password-randomizer.ts b/libs/tools/generator/core/src/engine/password-randomizer.ts index c1e9aed7b8b..a9612d2fb45 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.ts @@ -1,10 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; -import { GenerationRequest } from "@bitwarden/common/tools/types"; import { CredentialGenerator, + GenerateRequest, GeneratedCredential, PassphraseGenerationOptions, PasswordGenerationOptions, @@ -69,27 +69,39 @@ export class PasswordRandomizer } generate( - request: GenerationRequest, + request: GenerateRequest, settings: PasswordGenerationOptions, ): Promise; generate( - request: GenerationRequest, + request: GenerateRequest, settings: PassphraseGenerationOptions, ): Promise; async generate( - _request: GenerationRequest, + request: GenerateRequest, settings: PasswordGenerationOptions | PassphraseGenerationOptions, ) { if (isPasswordGenerationOptions(settings)) { - const request = optionsToRandomAsciiRequest(settings); - const password = await this.randomAscii(request); - - return new GeneratedCredential(password, "password", Date.now()); + const req = optionsToRandomAsciiRequest(settings); + const password = await this.randomAscii(req); + + return new GeneratedCredential( + password, + "password", + Date.now(), + request.source, + request.website, + ); } else if (isPassphraseGenerationOptions(settings)) { - const request = optionsToEffWordListRequest(settings); - const passphrase = await this.randomEffLongWords(request); - - return new GeneratedCredential(passphrase, "passphrase", Date.now()); + const req = optionsToEffWordListRequest(settings); + const passphrase = await this.randomEffLongWords(req); + + return new GeneratedCredential( + passphrase, + "passphrase", + Date.now(), + request.source, + request.website, + ); } throw new Error("Invalid settings received by generator."); diff --git a/libs/tools/generator/core/src/engine/username-randomizer.ts b/libs/tools/generator/core/src/engine/username-randomizer.ts index df608553839..d13066c7e55 100644 --- a/libs/tools/generator/core/src/engine/username-randomizer.ts +++ b/libs/tools/generator/core/src/engine/username-randomizer.ts @@ -1,7 +1,11 @@ import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; -import { GenerationRequest } from "@bitwarden/common/tools/types"; -import { CredentialGenerator, EffUsernameGenerationOptions, GeneratedCredential } from "../types"; +import { + CredentialGenerator, + EffUsernameGenerationOptions, + GenerateRequest, + GeneratedCredential, +} from "../types"; import { Randomizer } from "./abstractions"; import { WordsRequest } from "./types"; @@ -51,14 +55,20 @@ export class UsernameRandomizer implements CredentialGenerator(); + +describe("email - catchall generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(catchall.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = catchall.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: CatchallGenerationOptions = { + catchallType: "random", + catchallDomain: "example.com", + }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a catchall constraints", () => { + const context = { defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(CatchallConstraints); + }); + + it("extracts the domain from context.email", () => { + const context = { email: "foo@example.com", defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context) as CatchallConstraints; + + expect(constraints.domain).toEqual("example.com"); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/email/catchall.ts b/libs/tools/generator/core/src/metadata/email/catchall.ts new file mode 100644 index 00000000000..0711e5c3719 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/catchall.ts @@ -0,0 +1,70 @@ +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { EmailRandomizer } from "../../engine"; +import { CatchallConstraints } from "../../policies/catchall-constraints"; +import { + CatchallGenerationOptions, + CredentialGenerator, + GeneratorDependencyProvider, +} from "../../types"; +import { Algorithm, Type, Profile } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const catchall: GeneratorMetadata = deepFreeze({ + id: Algorithm.catchall, + category: Type.email, + weight: 210, + i18nKeys: { + name: "catchallEmail", + description: "catchallEmailDesc", + credentialType: "email", + generateCredential: "generateEmail", + credentialGenerated: "emailGenerated", + copyCredential: "copyEmail", + useCredential: "useThisEmail", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "catchallGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "catchallType", + "catchallDomain", + ]), + state: GENERATOR_DISK, + initial: { + catchallType: "random", + catchallDomain: "", + }, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + }, + constraints: { + default: { catchallDomain: { minLength: 1 } }, + create(_policies, context) { + return new CatchallConstraints(context.email ?? ""); + }, + }, + }, + }, +}); + +export default catchall; diff --git a/libs/tools/generator/core/src/metadata/email/forwarder.ts b/libs/tools/generator/core/src/metadata/email/forwarder.ts new file mode 100644 index 00000000000..1dfc219d466 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/forwarder.ts @@ -0,0 +1,4 @@ +// Forwarders are pending integration with the extension API +// +// They use the 300-block of weights and derive their metadata +// using logic similar to `toCredentialGeneratorConfiguration` diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts new file mode 100644 index 00000000000..2ac7645ed30 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts @@ -0,0 +1,65 @@ +import { mock } from "jest-mock-extended"; + +import { EmailRandomizer } from "../../engine"; +import { SubaddressConstraints } from "../../policies/subaddress-constraints"; +import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import plusAddress from "./plus-address"; + +const dependencyProvider = mock(); + +describe("email - plus address generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(plusAddress.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = plusAddress.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: SubaddressGenerationOptions = { + subaddressType: "random", + subaddressEmail: "foo@example.com", + }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a subaddress constraints", () => { + const context = { defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(SubaddressConstraints); + }); + + it("sets the constraint email to context.email", () => { + const context = { email: "bar@example.com", defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context) as SubaddressConstraints; + + expect(constraints.email).toEqual("bar@example.com"); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.ts b/libs/tools/generator/core/src/metadata/email/plus-address.ts new file mode 100644 index 00000000000..0db0acd415c --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/plus-address.ts @@ -0,0 +1,72 @@ +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { EmailRandomizer } from "../../engine"; +import { SubaddressConstraints } from "../../policies/subaddress-constraints"; +import { + CredentialGenerator, + GeneratorDependencyProvider, + SubaddressGenerationOptions, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const plusAddress: GeneratorMetadata = deepFreeze({ + id: Algorithm.plusAddress, + category: Type.email, + weight: 200, + i18nKeys: { + name: "plusAddressedEmail", + description: "plusAddressedEmailDesc", + credentialType: "email", + generateCredential: "generateEmail", + credentialGenerated: "emailGenerated", + copyCredential: "copyEmail", + useCredential: "useThisEmail", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "subaddressGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "subaddressType", + "subaddressEmail", + ]), + state: GENERATOR_DISK, + initial: { + subaddressType: "random", + subaddressEmail: "", + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + }, + constraints: { + default: {}, + create(_policy, context) { + return new SubaddressConstraints(context.email ?? ""); + }, + }, + }, + }, +}); + +export default plusAddress; diff --git a/libs/tools/generator/core/src/metadata/generator-metadata.ts b/libs/tools/generator/core/src/metadata/generator-metadata.ts new file mode 100644 index 00000000000..9296d30430e --- /dev/null +++ b/libs/tools/generator/core/src/metadata/generator-metadata.ts @@ -0,0 +1,29 @@ +import { CredentialGenerator, GeneratorDependencyProvider } from "../types"; + +import { AlgorithmMetadata } from "./algorithm-metadata"; +import { Profile } from "./data"; +import { ProfileMetadata } from "./profile-metadata"; + +/** Extends the algorithm metadata with storage and engine configurations. + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; + */ +export type GeneratorMetadata = AlgorithmMetadata & { + /** An algorithm that generates credentials when ran. */ + engine: { + /** Factory for the generator + */ + create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator; + }; + + /** Defines parameters for credential generation */ + profiles: { + /** profiles supported by this generator; when `undefined`, + * the generator does not support the profile. + */ + [K in keyof typeof Profile]?: ProfileMetadata; + }; +}; diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts new file mode 100644 index 00000000000..57961a60033 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -0,0 +1,102 @@ +import { mock } from "jest-mock-extended"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; + +import { PasswordRandomizer } from "../../engine"; +import { PassphrasePolicyConstraints } from "../../policies"; +import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import effPassphrase from "./eff-word-list"; + +const dependencyProvider = mock(); + +describe("password - eff words generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(effPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = effPassphrase.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: PassphraseGenerationOptions = { ...accountProfile.storage.initial }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a passphrase policy constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints); + }); + + it("forwards the policy to the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + data: { + minNumberWords: 6, + capitalize: false, + includeNumber: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.numWords.min).toEqual(6); + }); + + it("combines multiple policies in the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + data: { + minNumberWords: 6, + capitalize: false, + includeNumber: false, + }, + }, + { + type: PolicyType.PasswordGenerator, + data: { + minNumberWords: 3, + capitalize: true, + includeNumber: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.numWords.min).toEqual(6); + expect(constraints.constraints.capitalize.requiredValue).toEqual(true); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts new file mode 100644 index 00000000000..fc86032bf6b --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts @@ -0,0 +1,91 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; + +import { PasswordRandomizer } from "../../engine"; +import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; +import { + CredentialGenerator, + GeneratorDependencyProvider, + PassphraseGenerationOptions, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const passphrase: GeneratorMetadata = { + id: Algorithm.passphrase, + category: Type.password, + weight: 110, + i18nKeys: { + name: "passphrase", + credentialType: "passphrase", + generateCredential: "generatePassphrase", + credentialGenerated: "passphraseGenerated", + copyCredential: "copyPassphrase", + useCredential: "useThisPassphrase", + }, + capabilities: { + autogenerate: false, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "passphraseGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "numWords", + "wordSeparator", + "capitalize", + "includeNumber", + ]), + state: GENERATOR_DISK, + initial: { + numWords: 6, + wordSeparator: "-", + capitalize: false, + includeNumber: false, + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + constraints: { + type: PolicyType.PasswordGenerator, + default: { + wordSeparator: { maxLength: 1 }, + numWords: { + min: 3, + max: 20, + recommendation: 6, + }, + }, + create(policies, context) { + const initial = { + minNumberWords: 0, + capitalize: false, + includeNumber: false, + }; + const policy = policies.reduce(passphraseLeastPrivilege, initial); + const constraints = new PassphrasePolicyConstraints(policy, context.defaultConstraints); + return constraints; + }, + }, + }, + }, +}; + +export default passphrase; diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts new file mode 100644 index 00000000000..d91ceaac248 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -0,0 +1,105 @@ +import { mock } from "jest-mock-extended"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; + +import { PasswordRandomizer } from "../../engine"; +import { DynamicPasswordPolicyConstraints } from "../../policies"; +import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import password from "./random-password"; + +const dependencyProvider = mock(); + +describe("password - characters generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(password.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = password.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: PasswordGenerationOptions = { ...accountProfile.storage.initial }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a passphrase policy constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(DynamicPasswordPolicyConstraints); + }); + + it("forwards the policy to the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + enabled: true, + data: { + minLength: 10, + capitalize: false, + useNumbers: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.length.min).toEqual(10); + }); + + it("combines multiple policies in the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + enabled: true, + data: { + minLength: 14, + useSpecial: false, + useNumbers: false, + }, + }, + { + type: PolicyType.PasswordGenerator, + enabled: true, + data: { + minLength: 10, + useSpecial: true, + includeNumber: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.length.min).toEqual(14); + expect(constraints.constraints.special.requiredValue).toEqual(true); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts new file mode 100644 index 00000000000..693236b0967 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/random-password.ts @@ -0,0 +1,117 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { PasswordRandomizer } from "../../engine"; +import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; +import { + CredentialGenerator, + GeneratorDependencyProvider, + PasswordGeneratorSettings, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const password: GeneratorMetadata = deepFreeze({ + id: Algorithm.password, + category: Type.password, + weight: 100, + i18nKeys: { + name: "password", + generateCredential: "generatePassword", + credentialGenerated: "passwordGenerated", + credentialType: "password", + copyCredential: "copyPassword", + useCredential: "useThisPassword", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "passwordGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "length", + "ambiguous", + "uppercase", + "minUppercase", + "lowercase", + "minLowercase", + "number", + "minNumber", + "special", + "minSpecial", + ]), + state: GENERATOR_DISK, + initial: { + length: 14, + ambiguous: true, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + number: true, + minNumber: 1, + special: false, + minSpecial: 0, + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + }, + constraints: { + type: PolicyType.PasswordGenerator, + default: { + length: { + min: 5, + max: 128, + recommendation: 14, + }, + minNumber: { + min: 0, + max: 9, + }, + minSpecial: { + min: 0, + max: 9, + }, + }, + create(policies, context) { + const initial = { + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }; + const policy = policies.reduce(passwordLeastPrivilege, initial); + const constraints = new DynamicPasswordPolicyConstraints( + policy, + context.defaultConstraints, + ); + return constraints; + }, + }, + }, + }, +}); + +export default password; diff --git a/libs/tools/generator/core/src/metadata/profile-metadata.ts b/libs/tools/generator/core/src/metadata/profile-metadata.ts new file mode 100644 index 00000000000..4ac9139f632 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/profile-metadata.ts @@ -0,0 +1,80 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { SiteId } from "@bitwarden/common/tools/extension"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; +import { Constraints } from "@bitwarden/common/tools/types"; + +import { GeneratorConstraints } from "../types"; + +export type ProfileContext = { + /** The email address for the current user; + * `undefined` when no email is available. + */ + email?: string; + + /** Default application limits for the profile */ + defaultConstraints: Constraints; +}; + +type ProfileConstraints = { + /** The key used to locate this profile's policies in the admin console. + * When this type is undefined, no policy is defined for the profile. + */ + type?: PolicyType; + + /** default application limits for this profile; these are overridden + * by the policy + */ + default: Constraints; + + /** Constructs generator constraints from a policy. + * @param policies the administrative policy to apply to the provided constraints + * When `type` is undefined then `policy` is `undefined` this is an empty array. + * @param defaultConstraints application constraints; typically those defined in + * the `default` member, above. + * @returns the generator constraints to apply to this profile's options. + */ + create: (policies: Policy[], context: ProfileContext) => GeneratorConstraints; +}; + +/** Generator profiles partition generator operations + * according to where they're used within the password + * manager. Core profiles store their data using the + * generator's system storage. + */ +export type CoreProfileMetadata = { + /** distinguishes profile metadata types */ + type: "core"; + + /** plaintext import buffer */ + import?: ObjectKey, Options> & { format: "plain" }; + + /** persistent storage location */ + storage: ObjectKey; + + /** policy enforced when saving the options */ + constraints: ProfileConstraints; +}; + +/** Generator profiles partition generator operations + * according to where they're used within the password + * manager. Extension profiles store their data + * using the extension system. + */ +export type ExtensionProfileMetadata = { + /** distinguishes profile metadata types */ + type: "extension"; + + /** The extension site described by this metadata */ + site: Site; + + constraints: ProfileConstraints; +}; + +/** Generator profiles partition generator operations + * according to where they're used within the password + * manager + */ +export type ProfileMetadata = + | CoreProfileMetadata + | ExtensionProfileMetadata; diff --git a/libs/tools/generator/core/src/metadata/type.ts b/libs/tools/generator/core/src/metadata/type.ts new file mode 100644 index 00000000000..924b92883e5 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/type.ts @@ -0,0 +1,28 @@ +import { VendorId } from "@bitwarden/common/tools/extension"; + +import { AlgorithmsByType, Profile, Type } from "./data"; + +/** categorizes credentials according to their use-case outside of Bitwarden */ +export type CredentialType = keyof typeof Type; + +/** categorizes credentials according to their expected use-case within Bitwarden */ +export type GeneratorProfile = keyof typeof Profile; + +/** A type of password that may be generated by the credential generator. */ +export type PasswordAlgorithm = (typeof AlgorithmsByType.password)[number]; + +/** A type of username that may be generated by the credential generator. */ +export type UsernameAlgorithm = (typeof AlgorithmsByType.username)[number]; + +/** A type of email address that may be generated by the credential generator. */ +export type EmailAlgorithm = (typeof AlgorithmsByType.email)[number] | ForwarderExtensionId; + +/** Identifies a forwarding service */ +export type ForwarderExtensionId = { forwarder: VendorId }; + +/** A type of credential that can be generated by the credential generator. */ +// this is defined in terms of `AlgorithmsByType` to typecheck the keys of +// `AlgorithmsByType` against the keys of `CredentialType`. +export type CredentialAlgorithm = + | (typeof AlgorithmsByType)[CredentialType][number] + | ForwarderExtensionId; diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts new file mode 100644 index 00000000000..aba9680a448 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts @@ -0,0 +1,58 @@ +import { mock } from "jest-mock-extended"; + +import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; + +import { UsernameRandomizer } from "../../engine"; +import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import effWordList from "./eff-word-list"; + +const dependencyProvider = mock(); + +describe("username - eff words generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(effWordList.engine.create(dependencyProvider)).toBeInstanceOf(UsernameRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = effWordList.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: EffUsernameGenerationOptions = { + wordCapitalize: true, + wordIncludeNumber: true, + }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a effWordList constraints", () => { + const context = { defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(IdentityConstraint); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.ts new file mode 100644 index 00000000000..6373daf8ed5 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.ts @@ -0,0 +1,70 @@ +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { UsernameRandomizer } from "../../engine"; +import { + CredentialGenerator, + EffUsernameGenerationOptions, + GeneratorDependencyProvider, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const effWordList: GeneratorMetadata = deepFreeze({ + id: Algorithm.username, + category: Type.username, + weight: 400, + i18nKeys: { + name: "randomWord", + credentialType: "username", + generateCredential: "generateUsername", + credentialGenerated: "usernameGenerated", + copyCredential: "copyUsername", + useCredential: "useThisUsername", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new UsernameRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "effUsernameGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "wordCapitalize", + "wordIncludeNumber", + ]), + state: GENERATOR_DISK, + initial: { + wordCapitalize: false, + wordIncludeNumber: false, + website: null, + }, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + }, + constraints: { + default: {}, + create(_policies, _context) { + return new IdentityConstraint(); + }, + }, + }, + }, +}); + +export default effWordList; diff --git a/libs/tools/generator/core/src/metadata/util.spec.ts b/libs/tools/generator/core/src/metadata/util.spec.ts new file mode 100644 index 00000000000..2283699140b --- /dev/null +++ b/libs/tools/generator/core/src/metadata/util.spec.ts @@ -0,0 +1,218 @@ +import { VendorId } from "@bitwarden/common/tools/extension"; + +import { Algorithm, AlgorithmsByType } from "./data"; +import { ProfileMetadata } from "./profile-metadata"; +import { + isPasswordAlgorithm, + isUsernameAlgorithm, + isForwarderExtensionId, + isEmailAlgorithm, + isSameAlgorithm, + isCoreProfile, + isForwarderProfile, +} from "./util"; + +describe("credential generator metadata utility functions", () => { + describe("isPasswordAlgorithm", () => { + it("returns `true` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isPasswordAlgorithm(algorithm)).toBe(true); + } + }); + + it("returns `false` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isPasswordAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isPasswordAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is a forwarder extension", () => { + expect(isPasswordAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false); + }); + }); + + describe("isUsernameAlgorithm", () => { + it("returns `false` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isUsernameAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isUsernameAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isUsernameAlgorithm(algorithm)).toBe(true); + } + }); + + it("returns `false` when the algorithm is a forwarder extension", () => { + expect(isUsernameAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false); + }); + }); + + describe("isForwarderExtensionId", () => { + it("returns `false` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isForwarderExtensionId(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isForwarderExtensionId(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isForwarderExtensionId(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is a forwarder extension", () => { + expect(isForwarderExtensionId({ forwarder: "bitwarden" as VendorId })).toBe(true); + }); + }); + + describe("isEmailAlgorithm", () => { + it("returns `false` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isEmailAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isEmailAlgorithm(algorithm)).toBe(true); + } + }); + + it("returns `false` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isEmailAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is a forwarder extension", () => { + expect(isEmailAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(true); + }); + }); + + describe("isSameAlgorithm", () => { + it("returns `true` when the algorithms are equal", () => { + // identical object + expect(isSameAlgorithm(Algorithm.catchall, Algorithm.catchall)).toBe(true); + + // equal object + expect(isSameAlgorithm(Algorithm.catchall, `${Algorithm.catchall}`)).toBe(true); + }); + + it("returns `false` when the algorithms are different", () => { + // not an exhaustive list + expect(isSameAlgorithm(Algorithm.catchall, Algorithm.passphrase)).toBe(false); + expect(isSameAlgorithm(Algorithm.passphrase, Algorithm.password)).toBe(false); + expect(isSameAlgorithm(Algorithm.password, Algorithm.plusAddress)).toBe(false); + expect(isSameAlgorithm(Algorithm.plusAddress, Algorithm.username)).toBe(false); + expect(isSameAlgorithm(Algorithm.username, Algorithm.passphrase)).toBe(false); + }); + + it("returns `true` when the algorithms refer to a forwarder with a matching vendor", () => { + const someVendor = { forwarder: "bitwarden" as VendorId }; + const sameVendor = { forwarder: "bitwarden" as VendorId }; + expect(isSameAlgorithm(someVendor, sameVendor)).toBe(true); + }); + + it("returns `false` when the algorithms refer to a forwarder with a different vendor", () => { + const someVendor = { forwarder: "bitwarden" as VendorId }; + const sameVendor = { forwarder: "bytewarden" as VendorId }; + expect(isSameAlgorithm(someVendor, sameVendor)).toBe(false); + }); + + it("returns `false` when the algorithms refer to a forwarder and a core algorithm", () => { + const someVendor = { forwarder: "bitwarden" as VendorId }; + // not an exhaustive list + expect(isSameAlgorithm(someVendor, Algorithm.plusAddress)).toBe(false); + expect(isSameAlgorithm(Algorithm.username, someVendor)).toBe(false); + }); + }); + + describe("isCoreProfile", () => { + it("returns `true` when the profile's type is `core`", () => { + const profile: ProfileMetadata = { + type: "core", + storage: null, + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isCoreProfile(profile)).toBe(true); + }); + + it("returns `false` when the profile's type is `extension`", () => { + const profile: ProfileMetadata = { + type: "extension", + site: "forwarder", + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isCoreProfile(profile)).toBe(false); + }); + }); + + describe("isForwarderProfile", () => { + it("returns `false` when the profile's type is `core`", () => { + const profile: ProfileMetadata = { + type: "core", + storage: null, + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isForwarderProfile(profile)).toBe(false); + }); + + it("returns `true` when the profile's type is `extension` and the site is `forwarder`", () => { + const profile: ProfileMetadata = { + type: "extension", + site: "forwarder", + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isForwarderProfile(profile)).toBe(true); + }); + + it("returns `false` when the profile's type is `extension` and the site is not `forwarder`", () => { + const profile: ProfileMetadata = { + type: "extension", + site: "not-a-forwarder" as any, + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isForwarderProfile(profile)).toBe(false); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/util.ts b/libs/tools/generator/core/src/metadata/util.ts new file mode 100644 index 00000000000..e85061720ad --- /dev/null +++ b/libs/tools/generator/core/src/metadata/util.ts @@ -0,0 +1,60 @@ +import { AlgorithmsByType } from "./data"; +import { CoreProfileMetadata, ExtensionProfileMetadata, ProfileMetadata } from "./profile-metadata"; +import { + CredentialAlgorithm, + EmailAlgorithm, + ForwarderExtensionId, + PasswordAlgorithm, + UsernameAlgorithm, +} from "./type"; + +/** Returns true when the input algorithm is a password algorithm. */ +export function isPasswordAlgorithm( + algorithm: CredentialAlgorithm, +): algorithm is PasswordAlgorithm { + return AlgorithmsByType.password.includes(algorithm as any); +} + +/** Returns true when the input algorithm is a username algorithm. */ +export function isUsernameAlgorithm( + algorithm: CredentialAlgorithm, +): algorithm is UsernameAlgorithm { + return AlgorithmsByType.username.includes(algorithm as any); +} + +/** Returns true when the input algorithm is a forwarder integration. */ +export function isForwarderExtensionId( + algorithm: CredentialAlgorithm, +): algorithm is ForwarderExtensionId { + return algorithm && typeof algorithm === "object" && "forwarder" in algorithm; +} + +/** Returns true when the input algorithm is an email algorithm. */ +export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm { + return AlgorithmsByType.email.includes(algorithm as any) || isForwarderExtensionId(algorithm); +} + +/** Returns true when the algorithms are the same. */ +export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) { + if (lhs === rhs) { + return true; + } else if (isForwarderExtensionId(lhs) && isForwarderExtensionId(rhs)) { + return lhs.forwarder === rhs.forwarder; + } else { + return false; + } +} + +/** Returns true when the input describes a core profile. */ +export function isCoreProfile( + value: ProfileMetadata, +): value is CoreProfileMetadata { + return value.type === "core"; +} + +/** Returns true when the input describes a forwarder extension profile. */ +export function isForwarderProfile( + value: ProfileMetadata, +): value is ExtensionProfileMetadata { + return value.type === "extension" && value.site === "forwarder"; +} diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts index f61db0b33ec..f37a8b21a3f 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts @@ -4,12 +4,8 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { - CredentialAlgorithm, - EmailAlgorithms, - PasswordAlgorithms, - UsernameAlgorithms, -} from "@bitwarden/generator-core"; + +import { CredentialAlgorithm, EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from ".."; /** Reduces policies to a set of available algorithms * @param policies the policies to reduce diff --git a/libs/tools/generator/core/src/policies/catchall-constraints.ts b/libs/tools/generator/core/src/policies/catchall-constraints.ts index 47476a304a9..7793180988d 100644 --- a/libs/tools/generator/core/src/policies/catchall-constraints.ts +++ b/libs/tools/generator/core/src/policies/catchall-constraints.ts @@ -24,7 +24,7 @@ export class CatchallConstraints implements StateConstraints> = {}; diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts index 160da706937..fce6dc300ce 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts @@ -1,5 +1,7 @@ +// FIXME: remove ts-strict-ignore once `FakeAccountService` implements ts strict support +// @ts-strict-ignore import { mock } from "jest-mock-extended"; -import { BehaviorSubject, filter, firstValueFrom, Subject } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -23,6 +25,7 @@ import { Generators } from "../data"; import { CredentialGeneratorConfiguration, GeneratedCredential, + GenerateRequest, GeneratorConstraints, } from "../types"; @@ -72,8 +75,10 @@ const SomeAlgorithm = "passphrase"; const SomeCategory = "password"; const SomeNameKey = "passphraseKey"; const SomeGenerateKey = "generateKey"; -const SomeGeneratedValueKey = "generatedValueKey"; +const SomeCredentialTypeKey = "credentialTypeKey"; +const SomeOnGeneratedMessageKey = "onGeneratedMessageKey"; const SomeCopyKey = "copyKey"; +const SomeUseGeneratedValueKey = "useGeneratedValueKey"; // fake the configuration const SomeConfiguration: CredentialGeneratorConfiguration = { @@ -81,16 +86,23 @@ const SomeConfiguration: CredentialGeneratorConfiguration { return { generate: (request, settings) => { - const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo; - const result = new GeneratedCredential(credential, SomeAlgorithm, SomeTime); + const result = new GeneratedCredential( + settings.foo, + SomeAlgorithm, + SomeTime, + request.source, + request.website, + ); return Promise.resolve(result); }, }; @@ -189,30 +201,8 @@ describe("CredentialGeneratorService", () => { }); describe("generate$", () => { - it("emits a generation for the active user when subscribed", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); - - const result = await generated.expectEmission(); - - expect(result).toEqual(new GeneratedCredential("value", SomeAlgorithm, SomeTime)); - }); - - it("follows the active user", async () => { - const someSettings = { foo: "some value" }; - const anotherSettings = { foo: "another value" }; - await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); - await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); + it("completes when `on$` completes", async () => { + await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -222,22 +212,24 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); + const on$ = new Subject(); + let complete = false; - await accountService.switchAccount(AnotherUser); - await generated.pauseUntilReceived(2); - generated.unsubscribe(); + // confirm no emission during subscription + generator.generate$(SomeConfiguration, { on$ }).subscribe({ + complete: () => { + complete = true; + }, + }); + on$.complete(); + await awaitAsync(); - expect(generated.emissions).toEqual([ - new GeneratedCredential("some value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another value", SomeAlgorithm, SomeTime), - ]); + expect(complete).toBeTruthy(); }); - it("emits a generation when the settings change", async () => { - const someSettings = { foo: "some value" }; - const anotherSettings = { foo: "another value" }; - await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); + it("includes request.source in the generated credential", async () => { + const settings = { foo: "value" }; + await stateProvider.setUserState(SettingsKey, settings, SomeUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -247,23 +239,15 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); + const on$ = new BehaviorSubject({ source: "some source" }); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); - await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser); - await generated.pauseUntilReceived(2); - generated.unsubscribe(); + const result = await generated.expectEmission(); - expect(generated.emissions).toEqual([ - new GeneratedCredential("some value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another value", SomeAlgorithm, SomeTime), - ]); + expect(result.source).toEqual("some source"); }); - // FIXME: test these when the fake state provider can create the required emissions - it.todo("errors when the settings error"); - it.todo("completes when the settings complete"); - - it("includes `website$`'s last emitted value", async () => { + it("includes request.website in the generated credential", async () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); const generator = new CredentialGeneratorService( @@ -275,18 +259,19 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const website$ = new BehaviorSubject("some website"); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ })); + const on$ = new BehaviorSubject({ website: "some website" }); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); const result = await generated.expectEmission(); - expect(result).toEqual( - new GeneratedCredential("some website|value", SomeAlgorithm, SomeTime), - ); + expect(result.website).toEqual("some website"); }); - it("errors when `website$` errors", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); + it("uses the active user's settings", async () => { + const someSettings = { foo: "some value" }; + const anotherSettings = { foo: "another value" }; + await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); + await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -296,44 +281,23 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const website$ = new BehaviorSubject("some website"); - let error = null; + const on$ = new BehaviorSubject({}); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); - generator.generate$(SomeConfiguration, { website$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - website$.error({ some: "error" }); - await awaitAsync(); + await accountService.switchAccount(AnotherUser); + on$.next({}); + await generated.pauseUntilReceived(2); + generated.unsubscribe(); - expect(error).toEqual({ some: "error" }); + expect(generated.emissions).toEqual([ + new GeneratedCredential("some value", SomeAlgorithm, SomeTime), + new GeneratedCredential("another value", SomeAlgorithm, SomeTime), + ]); }); - it("completes when `website$` completes", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const website$ = new BehaviorSubject("some website"); - let completed = false; - - generator.generate$(SomeConfiguration, { website$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - website$.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); + // FIXME: test these when the fake state provider can create the required emissions + it.todo("errors when the settings error"); + it.todo("completes when the settings complete"); it("emits a generation for a specific user when `user$` supplied", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); @@ -348,38 +312,17 @@ describe("CredentialGeneratorService", () => { accountService, ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); + const on$ = new Subject(); + const generated = new ObservableTracker( + generator.generate$(SomeConfiguration, { on$, userId$ }), + ); + on$.next({}); const result = await generated.expectEmission(); expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime)); }); - it("emits a generation for a specific user when `user$` emits", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const userId = new BehaviorSubject(SomeUser); - const userId$ = userId.pipe(filter((u) => !!u)); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); - - userId.next(AnotherUser); - const result = await generated.pauseUntilReceived(2); - - expect(result).toEqual([ - new GeneratedCredential("value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another", SomeAlgorithm, SomeTime), - ]); - }); - it("errors when `user$` errors", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); const generator = new CredentialGeneratorService( @@ -391,10 +334,11 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); + const on$ = new Subject(); const userId$ = new BehaviorSubject(SomeUser); let error = null; - generator.generate$(SomeConfiguration, { userId$ }).subscribe({ + generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({ error: (e: unknown) => { error = e; }, @@ -416,10 +360,11 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); + const on$ = new Subject(); const userId$ = new BehaviorSubject(SomeUser); let completed = false; - generator.generate$(SomeConfiguration, { userId$ }).subscribe({ + generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({ complete: () => { completed = true; }, @@ -442,7 +387,7 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const on$ = new Subject(); + const on$ = new Subject(); const results: any[] = []; // confirm no emission during subscription @@ -453,7 +398,7 @@ describe("CredentialGeneratorService", () => { expect(results.length).toEqual(0); // confirm forwarded emission - on$.next(); + on$.next({}); await awaitAsync(); expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]); @@ -463,7 +408,7 @@ describe("CredentialGeneratorService", () => { expect(results.length).toBe(1); // confirm forwarded emission takes latest value - on$.next(); + on$.next({}); await awaitAsync(); sub.unsubscribe(); @@ -484,7 +429,7 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const on$ = new Subject(); + const on$ = new Subject(); let error: any = null; // confirm no emission during subscription @@ -499,35 +444,8 @@ describe("CredentialGeneratorService", () => { expect(error).toEqual({ some: "error" }); }); - it("completes when `on$` completes", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const on$ = new Subject(); - let complete = false; - - // confirm no emission during subscription - generator.generate$(SomeConfiguration, { on$ }).subscribe({ - complete: () => { - complete = true; - }, - }); - on$.complete(); - await awaitAsync(); - - expect(complete).toBeTruthy(); - }); - // FIXME: test these when the fake state provider can delay its first emission it.todo("emits when settings$ become available if on$ is called before they're ready."); - it.todo("emits when website$ become available if on$ is called before they're ready."); }); describe("algorithms", () => { diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 405cd220ab7..6e80049870c 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -2,20 +2,15 @@ // @ts-strict-ignore import { BehaviorSubject, - combineLatest, - concat, concatMap, distinctUntilChanged, endWith, filter, - first, firstValueFrom, ignoreElements, map, Observable, ReplaySubject, - share, - skipUntil, switchMap, takeUntil, withLatestFrom, @@ -34,9 +29,9 @@ import { SingleUserDependency, UserDependency, } from "@bitwarden/common/tools/dependencies"; -import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration"; +import { IntegrationMetadata } from "@bitwarden/common/tools/integration"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { anyComplete } from "@bitwarden/common/tools/rx"; +import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { UserId } from "@bitwarden/common/types/guid"; @@ -57,6 +52,7 @@ import { CredentialPreference, isForwarderIntegration, ForwarderIntegration, + GenerateRequest, } from "../types"; import { CredentialGeneratorConfiguration as Configuration, @@ -69,19 +65,7 @@ import { PREFERENCES } from "./credential-preferences"; type Policy$Dependencies = UserDependency; type Settings$Dependencies = Partial; -type Generate$Dependencies = Simplify & Partial> & { - /** Emits the active website when subscribed. - * - * The generator does not respond to emissions of this interface; - * If it is provided, the generator blocks until a value becomes available. - * When `website$` is omitted, the generator uses the empty string instead. - * When `website$` completes, the generator completes. - * When `website$` errors, the generator forwards the error. - */ - website$?: Observable; - - integration$?: Observable; -}; +type Generate$Dependencies = Simplify & Partial>; type Algorithms$Dependencies = Partial; @@ -111,43 +95,20 @@ export class CredentialGeneratorService { /** Generates a stream of credentials * @param configuration determines which generator's settings are loaded - * @param dependencies.on$ when specified, a new credential is emitted when - * this emits. Otherwise, a new credential is emitted when the settings - * update. + * @param dependencies.on$ Required. A new credential is emitted when this emits. */ generate$( configuration: Readonly>, - dependencies?: Generate$Dependencies, + dependencies: Generate$Dependencies, ) { - // instantiate the engine const engine = configuration.engine.create(this.getDependencyProvider()); - - // stream blocks until all of these values are received - const website$ = dependencies?.website$ ?? new BehaviorSubject(null); - const request$ = website$.pipe(map((website) => ({ website }))); const settings$ = this.settings$(configuration, dependencies); - // if on$ triggers before settings are loaded, trigger as soon - // as they become available. - let readyOn$: Observable = null; - if (dependencies?.on$) { - const NO_EMISSIONS = {}; - const ready$ = combineLatest([settings$, request$]).pipe( - first(null, NO_EMISSIONS), - filter((value) => value !== NO_EMISSIONS), - share(), - ); - readyOn$ = concat( - dependencies.on$?.pipe(switchMap(() => ready$)), - dependencies.on$.pipe(skipUntil(ready$)), - ); - } - // generation proper - const generate$ = (readyOn$ ?? settings$).pipe( - withLatestFrom(request$, settings$), - concatMap(([, request, settings]) => engine.generate(request, settings)), - takeUntil(anyComplete([request$, settings$])), + const generate$ = dependencies.on$.pipe( + withLatestReady(settings$), + concatMap(([request, settings]) => engine.generate(request, settings)), + takeUntil(anyComplete([settings$])), ); return generate$; @@ -256,8 +217,10 @@ export class CredentialGeneratorService { category: generator.category, name: integration ? integration.name : this.i18nService.t(generator.nameKey), generate: this.i18nService.t(generator.generateKey), - generatedValue: this.i18nService.t(generator.generatedValueKey), + onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey), + credentialType: this.i18nService.t(generator.credentialTypeKey), copy: this.i18nService.t(generator.copyKey), + useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey), onlyOnRequest: generator.onlyOnRequest, request: generator.request, }; diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts index 22bfa31441e..08aec48a9e7 100644 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts @@ -34,11 +34,17 @@ export type AlgorithmInfo = { /* Localized generate button label */ generate: string; + /** Localized "credential generated" informational message */ + onGeneratedMessage: string; + /* Localized copy button label */ copy: string; + /* Localized dialog button label */ + useGeneratedValue: string; + /* Localized generated value label */ - generatedValue: string; + credentialType: string; /** Localized algorithm description */ description?: string; @@ -76,14 +82,22 @@ export type CredentialGeneratorInfo = { /** Localization key for the credential description*/ descriptionKey?: string; - /* Localization key for the generate command label */ + /** Localization key for the generate command label */ generateKey: string; - /* Localization key for the copy button label */ + /** Localization key for the copy button label */ copyKey: string; - /* Localization key for describing values generated by this generator */ - generatedValueKey: string; + /** Localization key for the "credential generated" informational message */ + onGeneratedMessageKey: string; + + /** Localized "use generated credential" button label */ + useGeneratedValueKey: string; + + /** Localization key for describing the kind of credential generated + * by this generator. + */ + credentialTypeKey: string; /** When true, credential generation must be explicitly requested. * @remarks this property is useful when credential generation diff --git a/libs/tools/generator/core/src/types/credential-generator.ts b/libs/tools/generator/core/src/types/credential-generator.ts index c95ff25afff..c421bbbff87 100644 --- a/libs/tools/generator/core/src/types/credential-generator.ts +++ b/libs/tools/generator/core/src/types/credential-generator.ts @@ -1,5 +1,4 @@ -import { GenerationRequest } from "@bitwarden/common/tools/types"; - +import { GenerateRequest } from "./generate-request"; import { GeneratedCredential } from "./generated-credential"; /** An algorithm that generates credentials. */ @@ -8,5 +7,5 @@ export type CredentialGenerator = { * @param request runtime parameters * @param settings stored parameters */ - generate: (request: GenerationRequest, settings: Settings) => Promise; + generate: (request: GenerateRequest, settings: Settings) => Promise; }; diff --git a/libs/tools/generator/core/src/types/generate-request.ts b/libs/tools/generator/core/src/types/generate-request.ts new file mode 100644 index 00000000000..c7d5bf9c41c --- /dev/null +++ b/libs/tools/generator/core/src/types/generate-request.ts @@ -0,0 +1,24 @@ +/** Contextual information about the application state when a generator is invoked. + */ +export type GenerateRequest = { + /** Traces the origin of the generation request. This parameter is + * copied to the generated credential. + * + * @remarks This parameter it is provided solely so that generator + * consumers can differentiate request sources from one another. + * It never affects the random output of the generator algorithms, + * and it is never communicated to 3rd party systems. It MAY be + * tracked in the generator history. + */ + source?: string; + + /** Traces the website associated with a generated credential. + * + * @remarks Random generators MUST NOT depend upon the website during credential + * generation. Non-random generators MAY include the website in the generated + * credential (e.g. a catchall email address). This parameter MAY be transmitted + * to 3rd party systems (e.g. as the description for a forwarding email). + * It MAY be tracked in the generator history. + */ + website?: string; +}; diff --git a/libs/tools/generator/core/src/types/generated-credential.ts b/libs/tools/generator/core/src/types/generated-credential.ts index 6d18a1c7892..99b864b9fd8 100644 --- a/libs/tools/generator/core/src/types/generated-credential.ts +++ b/libs/tools/generator/core/src/types/generated-credential.ts @@ -11,11 +11,15 @@ export class GeneratedCredential { * @param generationDate The date that the credential was generated. * Numeric values should are interpreted using {@link Date.valueOf} * semantics. + * @param source traces the origin of the request that generated this credential. + * @param website traces the website associated with the generated credential. */ constructor( readonly credential: string, readonly category: CredentialAlgorithm, generationDate: Date | number, + readonly source?: string, + readonly website?: string, ) { if (typeof generationDate === "number") { this.generationDate = new Date(generationDate); @@ -25,7 +29,7 @@ export class GeneratedCredential { } /** The date that the credential was generated */ - generationDate: Date; + readonly generationDate: Date; /** Constructs a credential from its `toJSON` representation */ static fromJSON(jsonValue: Jsonify) { @@ -38,6 +42,9 @@ export class GeneratedCredential { /** Serializes a credential to a JSON-compatible object */ toJSON() { + // omits the source and website because they were introduced to solve + // UI bugs and it's not yet known whether there's a desire to support + // them in the generator history view. return { credential: this.credential, category: this.category, diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts index 48272cbf602..3e392257b0c 100644 --- a/libs/tools/generator/core/src/types/index.ts +++ b/libs/tools/generator/core/src/types/index.ts @@ -6,6 +6,7 @@ export * from "./credential-generator"; export * from "./credential-generator-configuration"; export * from "./eff-username-generator-options"; export * from "./forwarder-options"; +export * from "./generate-request"; export * from "./generator-constraints"; export * from "./generated-credential"; export * from "./generator-options"; diff --git a/libs/tools/generator/core/src/types/password-generation-options.ts b/libs/tools/generator/core/src/types/password-generation-options.ts index 76e8827d4de..7a8a538c409 100644 --- a/libs/tools/generator/core/src/types/password-generation-options.ts +++ b/libs/tools/generator/core/src/types/password-generation-options.ts @@ -2,58 +2,58 @@ */ export type PasswordGeneratorSettings = { /** The length of the password selected by the user */ - length: number; + length?: number; /** `true` when ambiguous characters may be included in the output. * `false` when ambiguous characters should not be included in the output. */ - ambiguous: boolean; + ambiguous?: boolean; /** `true` when uppercase ASCII characters should be included in the output * This value defaults to `false. */ - uppercase: boolean; + uppercase?: boolean; /** The minimum number of uppercase characters to include in the output. * The value is ignored when `uppercase` is `false`. * The value defaults to 1 when `uppercase` is `true`. */ - minUppercase: number; + minUppercase?: number; /** `true` when lowercase ASCII characters should be included in the output. * This value defaults to `false`. */ - lowercase: boolean; + lowercase?: boolean; /** The minimum number of lowercase characters to include in the output. * The value defaults to 1 when `lowercase` is `true`. * The value defaults to 0 when `lowercase` is `false`. */ - minLowercase: number; + minLowercase?: number; /** Whether or not to include ASCII digits in the output * This value defaults to `true` when `minNumber` is at least 1. * This value defaults to `false` when `minNumber` is less than 1. */ - number: boolean; + number?: boolean; /** The minimum number of digits to include in the output. * The value defaults to 1 when `number` is `true`. * The value defaults to 0 when `number` is `false`. */ - minNumber: number; + minNumber?: number; /** Whether or not to include special characters in the output. * This value defaults to `true` when `minSpecial` is at least 1. * This value defaults to `false` when `minSpecial` is less than 1. */ - special: boolean; + special?: boolean; /** The minimum number of special characters to include in the output. * This value defaults to 1 when `special` is `true`. * This value defaults to 0 when `special` is `false`. */ - minSpecial: number; + minSpecial?: number; }; /** Request format for password credential generation. diff --git a/libs/tools/generator/core/src/util.ts b/libs/tools/generator/core/src/util.ts index 98c2e8ab283..4b6041ffeba 100644 --- a/libs/tools/generator/core/src/util.ts +++ b/libs/tools/generator/core/src/util.ts @@ -107,7 +107,7 @@ export function optionsToRandomAsciiRequest(options: PasswordGenerationOptions) DefaultPasswordGenerationOptions.special, DefaultPasswordGenerationOptions.minSpecial, ), - ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous, + ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous!, all: 0, }; diff --git a/libs/tools/generator/core/tsconfig.json b/libs/tools/generator/core/tsconfig.json index 6eec2cc24a9..a95b588686f 100644 --- a/libs/tools/generator/core/tsconfig.json +++ b/libs/tools/generator/core/tsconfig.json @@ -1,5 +1,13 @@ { - "extends": "../../../shared/tsconfig.libs", + "extends": "../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../admin-console/src/common"], + "@bitwarden/auth/common": ["../../../auth/src/common"], + "@bitwarden/common/*": ["../../../common/src/*"], + "@bitwarden/key-management": ["../../../key-management/src"] + } + }, "include": [ "src", "../extensions/src/history/generator-history.abstraction.ts", diff --git a/libs/tools/generator/extensions/history/jest.config.js b/libs/tools/generator/extensions/history/jest.config.js index d257f3871c7..f90801cd7c4 100644 --- a/libs/tools/generator/extensions/history/jest.config.js +++ b/libs/tools/generator/extensions/history/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../../../../shared/tsconfig.libs"); +const { compilerOptions } = require("../../../../shared/tsconfig.spec"); /** @type {import('jest').Config} */ module.exports = { diff --git a/libs/tools/generator/extensions/history/tsconfig.json b/libs/tools/generator/extensions/history/tsconfig.json index 5cb90260371..5fc1caf014f 100644 --- a/libs/tools/generator/extensions/history/tsconfig.json +++ b/libs/tools/generator/extensions/history/tsconfig.json @@ -1,5 +1,14 @@ { - "extends": "../../../../shared/tsconfig.libs", + "extends": "../../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../../admin-console/src/common"], + "@bitwarden/auth/common": ["../../../../auth/src/common"], + "@bitwarden/common/*": ["../../../../common/src/*"], + "@bitwarden/generator-core": ["../../../../tools/generator/core/src"], + "@bitwarden/key-management": ["../../../../key-management/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/tools/generator/extensions/legacy/jest.config.js b/libs/tools/generator/extensions/legacy/jest.config.js index d257f3871c7..f90801cd7c4 100644 --- a/libs/tools/generator/extensions/legacy/jest.config.js +++ b/libs/tools/generator/extensions/legacy/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../../../../shared/tsconfig.libs"); +const { compilerOptions } = require("../../../../shared/tsconfig.spec"); /** @type {import('jest').Config} */ module.exports = { diff --git a/libs/tools/generator/extensions/legacy/tsconfig.json b/libs/tools/generator/extensions/legacy/tsconfig.json index 5cb90260371..9a09e28ea3d 100644 --- a/libs/tools/generator/extensions/legacy/tsconfig.json +++ b/libs/tools/generator/extensions/legacy/tsconfig.json @@ -1,5 +1,16 @@ { - "extends": "../../../../shared/tsconfig.libs", + "extends": "../../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../../admin-console/src/common"], + "@bitwarden/auth/common": ["../../../../auth/src/common"], + "@bitwarden/common/*": ["../../../../common/src/*"], + "@bitwarden/generator-core": ["../../../../tools/generator/core/src"], + "@bitwarden/generator-history": ["../../../../tools/generator/extensions/history/src"], + "@bitwarden/generator-navigation": ["../../../../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../../../../key-management/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/tools/generator/extensions/navigation/jest.config.js b/libs/tools/generator/extensions/navigation/jest.config.js index d257f3871c7..f90801cd7c4 100644 --- a/libs/tools/generator/extensions/navigation/jest.config.js +++ b/libs/tools/generator/extensions/navigation/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../../../../shared/tsconfig.libs"); +const { compilerOptions } = require("../../../../shared/tsconfig.spec"); /** @type {import('jest').Config} */ module.exports = { diff --git a/libs/tools/generator/extensions/navigation/tsconfig.json b/libs/tools/generator/extensions/navigation/tsconfig.json index 5cb90260371..5fc1caf014f 100644 --- a/libs/tools/generator/extensions/navigation/tsconfig.json +++ b/libs/tools/generator/extensions/navigation/tsconfig.json @@ -1,5 +1,14 @@ { - "extends": "../../../../shared/tsconfig.libs", + "extends": "../../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../../admin-console/src/common"], + "@bitwarden/auth/common": ["../../../../auth/src/common"], + "@bitwarden/common/*": ["../../../../common/src/*"], + "@bitwarden/generator-core": ["../../../../tools/generator/core/src"], + "@bitwarden/key-management": ["../../../../key-management/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/tools/send/send-ui/jest.config.js b/libs/tools/send/send-ui/jest.config.js index b68bda8d5ca..952e9ce0e2e 100644 --- a/libs/tools/send/send-ui/jest.config.js +++ b/libs/tools/send/send-ui/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../../../shared/tsconfig.libs"); +const { compilerOptions } = require("../../../shared/tsconfig.spec"); /** @type {import('jest').Config} */ module.exports = { diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index 636b7546af8..4b569532220 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -4,11 +4,13 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -25,7 +27,7 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { CredentialGeneratorService, Generators } from "@bitwarden/generator-core"; +import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core"; import { SendFormConfig } from "../../abstractions/send-form-config.service"; import { SendFormContainer } from "../../send-form-container"; @@ -89,11 +91,14 @@ export class SendOptionsComponent implements OnInit { private i18nService: I18nService, private toastService: ToastService, private generatorService: CredentialGeneratorService, + private accountService: AccountService, ) { this.sendFormContainer.registerChildForm("sendOptionsForm", this.sendOptionsForm); - this.policyService - .getAll$(PolicyType.SendOptions) + + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntilDestroyed(), ) @@ -116,8 +121,9 @@ export class SendOptionsComponent implements OnInit { } generatePassword = async () => { + const on$ = new BehaviorSubject({ source: "send" }); const generatedCredential = await firstValueFrom( - this.generatorService.generate$(Generators.password), + this.generatorService.generate$(Generators.password, { on$ }), ); this.sendOptionsForm.patchValue({ diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html index d7755546365..d244be15087 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html @@ -18,12 +18,12 @@

{{ send.name }} diff --git a/libs/tools/send/send-ui/tsconfig.json b/libs/tools/send/send-ui/tsconfig.json index c52bfd7b0df..e6d6680ad40 100644 --- a/libs/tools/send/send-ui/tsconfig.json +++ b/libs/tools/send/send-ui/tsconfig.json @@ -1,5 +1,22 @@ { - "extends": "../../../shared/tsconfig.libs", + "extends": "../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../admin-console/src/common"], + "@bitwarden/angular/*": ["../../../angular/src/*"], + "@bitwarden/auth/common": ["../../../auth/src/common"], + "@bitwarden/common/*": ["../../../common/src/*"], + "@bitwarden/components": ["../../../components/src"], + "@bitwarden/generator-components": ["../../../tools/generator/components/src"], + "@bitwarden/generator-core": ["../../../tools/generator/core/src"], + "@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../../../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../../../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../../../key-management/src"], + "@bitwarden/platform": ["../../../platform/src"], + "@bitwarden/ui-common": ["../../../ui/common/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/ui/README.md b/libs/ui/README.md new file mode 100644 index 00000000000..e245aac71cb --- /dev/null +++ b/libs/ui/README.md @@ -0,0 +1,5 @@ +# UI Foundation + +Core UI libraries maintained by the UI Foundation team. + +- _ui-common_: Low-level utilities for Angular applications. diff --git a/libs/ui/common/package.json b/libs/ui/common/package.json new file mode 100644 index 00000000000..f1b03a3ebd0 --- /dev/null +++ b/libs/ui/common/package.json @@ -0,0 +1,15 @@ +{ + "name": "@bitwarden/ui-common", + "version": "0.0.0", + "description": "Low-level utilities for Angular applications", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0" +} diff --git a/libs/ui/common/src/di/index.ts b/libs/ui/common/src/di/index.ts new file mode 100644 index 00000000000..7b0705f1364 --- /dev/null +++ b/libs/ui/common/src/di/index.ts @@ -0,0 +1,2 @@ +export * from "./safe-injection-token"; +export * from "./safe-provider"; diff --git a/libs/ui/common/src/di/safe-injection-token.ts b/libs/ui/common/src/di/safe-injection-token.ts new file mode 100644 index 00000000000..aad081e1c76 --- /dev/null +++ b/libs/ui/common/src/di/safe-injection-token.ts @@ -0,0 +1,14 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { InjectionToken } from "@angular/core"; + +declare const tag: unique symbol; +/** + * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. + * @remarks The default angular implementation does not use the generic type to define the structure of the object, + * so the structural type system will not complain about a mismatch in the type parameter. + * This is solved by assigning T to an arbitrary private property. + */ +export class SafeInjectionToken extends InjectionToken { + private readonly [tag]: T; +} diff --git a/libs/ui/common/src/di/safe-provider.ts b/libs/ui/common/src/di/safe-provider.ts new file mode 100644 index 00000000000..002aa69a500 --- /dev/null +++ b/libs/ui/common/src/di/safe-provider.ts @@ -0,0 +1,138 @@ +import { Provider } from "@angular/core"; +import { Constructor, Opaque } from "type-fest"; + +import { SafeInjectionToken } from "./safe-injection-token"; + +/** + * The return type of the {@link safeProvider} helper function. + * Used to distinguish a type safe provider definition from a non-type safe provider definition. + */ +export type SafeProvider = Opaque; + +// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 +type AbstractConstructor = abstract new (...args: any) => T; + +type MapParametersToDeps = { + [K in keyof T]: AbstractConstructor | SafeInjectionToken; +}; + +type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; + +/** + * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken + */ +type ProviderInstanceType = + T extends SafeInjectionToken + ? InstanceType> + : T extends Constructor | AbstractConstructor + ? InstanceType + : never; + +/** + * Represents a dependency provided with the useClass option. + */ +type SafeClassProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends Constructor>, + D extends MapParametersToDeps>, +> = { + provide: A; + useClass: I; + deps: D; +}; + +/** + * Represents a dependency provided with the useValue option. + */ +type SafeValueProvider, V extends SafeInjectionTokenType> = { + provide: A; + useValue: V; +}; + +/** + * Represents a dependency provided with the useFactory option. + */ +type SafeFactoryProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends (...args: any) => ProviderInstanceType, + D extends MapParametersToDeps>, +> = { + provide: A; + useFactory: I; + deps: D; + multi?: boolean; +}; + +/** + * Represents a dependency provided with the useExisting option. + */ +type SafeExistingProvider< + A extends Constructor | AbstractConstructor | SafeInjectionToken, + I extends Constructor> | AbstractConstructor>, +> = { + provide: A; + useExisting: I; +}; + +/** + * Represents a dependency where there is no abstract token, the token is the implementation + */ +type SafeConcreteProvider< + I extends Constructor, + D extends MapParametersToDeps>, +> = { + provide: I; + deps: D; +}; + +/** + * If useAngularDecorators: true is specified, do not require a deps array. + * This is a manual override for where @Injectable decorators are used + */ +type UseAngularDecorators = Omit & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators = T | UseAngularDecorators; + +/** + * A factory function that creates a provider for the ngModule providers array. + * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. + * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, + * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. + * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) + * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) + * @returns The exact same object without modification (pass-through). + */ +export const safeProvider = < + // types for useClass + AClass extends AbstractConstructor | SafeInjectionToken, + IClass extends Constructor>, + DClass extends MapParametersToDeps>, + // types for useValue + AValue extends SafeInjectionToken, + VValue extends SafeInjectionTokenType, + // types for useFactory + AFactory extends AbstractConstructor | SafeInjectionToken, + IFactory extends (...args: any) => ProviderInstanceType, + DFactory extends MapParametersToDeps>, + // types for useExisting + AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, + IExisting extends + | Constructor> + | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, +>( + provider: + | AllowAngularDecorators> + | SafeValueProvider + | AllowAngularDecorators> + | SafeExistingProvider + | AllowAngularDecorators> + | Constructor, +): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/platform/utils/safe-provider.type.spec.ts b/libs/ui/common/src/di/safe-provider.type.spec.ts similarity index 96% rename from libs/angular/src/platform/utils/safe-provider.type.spec.ts rename to libs/ui/common/src/di/safe-provider.type.spec.ts index 6fe6d0d0b6c..afc7071af1e 100644 --- a/libs/angular/src/platform/utils/safe-provider.type.spec.ts +++ b/libs/ui/common/src/di/safe-provider.type.spec.ts @@ -11,7 +11,7 @@ class FooFactory { } abstract class FooService { - createFoo: (str: string) => string; + abstract createFoo(str: string): string; } class DefaultFooService implements FooService { @@ -29,7 +29,7 @@ class BarFactory { } abstract class BarService { - createBar: (num: number) => number; + abstract createBar(num: number): number; } class DefaultBarService implements BarService { diff --git a/libs/components/src/shared/i18n.pipe.ts b/libs/ui/common/src/i18n.pipe.ts similarity index 77% rename from libs/components/src/shared/i18n.pipe.ts rename to libs/ui/common/src/i18n.pipe.ts index 91bf0b3198d..fdcfec0ceac 100644 --- a/libs/components/src/shared/i18n.pipe.ts +++ b/libs/ui/common/src/i18n.pipe.ts @@ -3,7 +3,13 @@ import { Pipe, PipeTransform } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; /** - * Temporarily duplicate this pipe + * Localizes the specified string. + * + * @example + * {{ 'key' | i18n }} + * + * @example + * {{ 'key' | i18n: 'param1' }} */ @Pipe({ name: "i18n", diff --git a/libs/ui/common/src/index.ts b/libs/ui/common/src/index.ts new file mode 100644 index 00000000000..97e55108116 --- /dev/null +++ b/libs/ui/common/src/index.ts @@ -0,0 +1,2 @@ +export * from "./di"; +export * from "./i18n.pipe"; diff --git a/libs/ui/common/tsconfig.json b/libs/ui/common/tsconfig.json new file mode 100644 index 00000000000..31062d41a1c --- /dev/null +++ b/libs/ui/common/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/common/*": ["../../common/src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/vault/jest.config.js b/libs/vault/jest.config.js index e960eed9f1a..e33c115e8dd 100644 --- a/libs/vault/jest.config.js +++ b/libs/vault/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index 54f194598e5..a5f2289b0c6 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -14,7 +14,6 @@ import { SshKeySectionComponent } from "./components/sshkey-section/sshkey-secti /** * The complete form for a cipher. Includes all the sub-forms from their respective section components. - * TODO: Add additional form sections as they are implemented. */ export type CipherForm = { itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"]; @@ -57,4 +56,12 @@ export abstract class CipherFormContainer { * @param updateFn - A function that takes the current cipherView and returns the updated cipherView */ abstract patchCipher(updateFn: (current: CipherView) => CipherView): void; + + /** + * Returns initial values for the CipherView, either from the config or the cached cipher + */ + abstract getInitialCipherView(): CipherView | null; + + /** Returns true when the `CipherFormContainer` was initialized with a cached cipher available. */ + abstract initializedWithCachedCipher(): boolean; } diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 72c4acb23cd..1af73b5a8b8 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -1,4 +1,6 @@ -import { importProvidersFrom } from "@angular/core"; +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { importProvidersFrom, signal } from "@angular/core"; import { action } from "@storybook/addon-actions"; import { applicationConfig, @@ -10,6 +12,7 @@ import { import { BehaviorSubject } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -18,6 +21,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { ClientType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -31,13 +35,15 @@ import { PasswordRepromptService, } from "@bitwarden/vault"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests"; import { CipherFormService } from "./abstractions/cipher-form.service"; import { TotpCaptureService } from "./abstractions/totp-capture.service"; import { CipherFormModule } from "./cipher-form.module"; import { CipherFormComponent } from "./components/cipher-form.component"; +import { CipherFormCacheService } from "./services/default-cipher-form-cache.service"; const defaultConfig: CipherFormConfig = { mode: "add", @@ -191,6 +197,25 @@ export default { activeAccount$: new BehaviorSubject({ email: "test@example.com" }), }, }, + { + provide: CipherFormCacheService, + useValue: { + getCachedCipherView: (): null => null, + initializedWithValue: false, + }, + }, + { + provide: ViewCacheService, + useValue: { + signal: () => signal(null), + }, + }, + { + provide: ConfigService, + useValue: { + getFeatureFlag: () => Promise.resolve(false), + }, + }, ], }), componentWrapperDecorator( diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts index 15784f1ca06..705c170933a 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -27,9 +27,12 @@ describe("AdditionalOptionsSectionComponent", () => { let passwordRepromptService: MockProxy; let passwordRepromptEnabled$: BehaviorSubject; + const getInitialCipherView = jest.fn(() => null); + beforeEach(async () => { - cipherFormProvider = mock(); + getInitialCipherView.mockClear(); + cipherFormProvider = mock({ getInitialCipherView }); passwordRepromptService = mock(); passwordRepromptEnabled$ = new BehaviorSubject(true); passwordRepromptService.enabled$ = passwordRepromptEnabled$; @@ -94,11 +97,11 @@ describe("AdditionalOptionsSectionComponent", () => { expect(component.additionalOptionsForm.disabled).toBe(true); }); - it("initializes 'additionalOptionsForm' with original cipher view values", () => { - (cipherFormProvider.originalCipherView as any) = { + it("initializes 'additionalOptionsForm' from `getInitialCipherValue`", () => { + getInitialCipherView.mockReturnValueOnce({ notes: "original notes", reprompt: 1, - } as CipherView; + } as CipherView); component.ngOnInit(); diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts index 04291a3e083..9c619ca2f84 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts @@ -77,11 +77,12 @@ export class AdditionalOptionsSectionComponent implements OnInit { } ngOnInit() { - if (this.cipherFormContainer.originalCipherView) { + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { this.additionalOptionsForm.patchValue({ - notes: this.cipherFormContainer.originalCipherView.notes, - reprompt: - this.cipherFormContainer.originalCipherView.reprompt === CipherRepromptType.Password, + notes: prefillCipher.notes, + reprompt: prefillCipher.reprompt === CipherRepromptType.Password, }); } diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts index f1e5af2868a..ea756d17c40 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts @@ -25,9 +25,11 @@ describe("AutofillOptionsComponent", () => { let domainSettingsService: MockProxy; let autofillSettingsService: MockProxy; let platformUtilsService: MockProxy; + const getInitialCipherView = jest.fn(() => null); beforeEach(async () => { - cipherFormContainer = mock(); + getInitialCipherView.mockClear(); + cipherFormContainer = mock({ getInitialCipherView }); liveAnnouncer = mock(); platformUtilsService = mock(); domainSettingsService = mock(); @@ -107,12 +109,14 @@ describe("AutofillOptionsComponent", () => { existingLogin.uri = "https://example.com"; existingLogin.match = UriMatchStrategy.Exact; - (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); - cipherFormContainer.originalCipherView.login = { + const cipher = new CipherView(); + cipher.login = { autofillOnPageLoad: true, uris: [existingLogin], } as LoginView; + getInitialCipherView.mockReturnValueOnce(cipher); + fixture.detectChanges(); expect(component.autofillOptionsForm.value.uris).toEqual([ @@ -138,12 +142,14 @@ describe("AutofillOptionsComponent", () => { existingLogin.uri = "https://example.com"; existingLogin.match = UriMatchStrategy.Exact; - (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); - cipherFormContainer.originalCipherView.login = { + const cipher = new CipherView(); + cipher.login = { autofillOnPageLoad: true, uris: [existingLogin], } as LoginView; + getInitialCipherView.mockReturnValueOnce(cipher); + fixture.detectChanges(); expect(component.autofillOptionsForm.value.uris).toEqual([ @@ -159,12 +165,14 @@ describe("AutofillOptionsComponent", () => { existingLogin.uri = "https://example.com"; existingLogin.match = UriMatchStrategy.Exact; - (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); - cipherFormContainer.originalCipherView.login = { + const cipher = new CipherView(); + cipher.login = { autofillOnPageLoad: true, uris: [existingLogin], } as LoginView; + getInitialCipherView.mockReturnValueOnce(cipher); + fixture.detectChanges(); expect(component.autofillOptionsForm.value.uris).toEqual([ diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index cf73f3dbed4..c3b2a0fb9f9 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -130,8 +130,9 @@ export class AutofillOptionsComponent implements OnInit { } ngOnInit() { - if (this.cipherFormContainer.originalCipherView?.login) { - this.initFromExistingCipher(this.cipherFormContainer.originalCipherView.login); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + if (prefillCipher) { + this.initFromExistingCipher(prefillCipher.login); } else { this.initNewCipher(); } @@ -142,17 +143,29 @@ export class AutofillOptionsComponent implements OnInit { } private initFromExistingCipher(existingLogin: LoginView) { + // The `uris` control is a FormArray which needs to dynamically + // add controls to the form. Doing this will trigger the `valueChanges` observable on the form + // and overwrite the `autofillOnPageLoad` value before it is set in the following `patchValue` call. + // Pass `false` to `addUri` to stop events from emitting when adding the URIs. existingLogin.uris?.forEach((uri) => { - this.addUri({ - uri: uri.uri, - matchDetection: uri.match, - }); + this.addUri( + { + uri: uri.uri, + matchDetection: uri.match, + }, + false, + false, + ); }); this.autofillOptionsForm.patchValue({ autofillOnPageLoad: existingLogin.autofillOnPageLoad, }); - if (this.cipherFormContainer.config.initialValues?.loginUri) { + // Only add the initial value when the cipher was not initialized from a cached state + if ( + this.cipherFormContainer.config.initialValues?.loginUri && + !this.cipherFormContainer.initializedWithCachedCipher() + ) { // Avoid adding the same uri again if it already exists if ( existingLogin.uris?.findIndex( @@ -197,9 +210,16 @@ export class AutofillOptionsComponent implements OnInit { * Adds a new URI input to the form. * @param uriFieldValue The initial value for the new URI input. * @param focusNewInput If true, the new URI input will be focused after being added. + * @param emitEvent When false, prevents the `valueChanges` & `statusChanges` observables from firing. */ - addUri(uriFieldValue: UriField = { uri: null, matchDetection: null }, focusNewInput = false) { - this.autofillOptionsForm.controls.uris.push(this.formBuilder.control(uriFieldValue)); + addUri( + uriFieldValue: UriField = { uri: null, matchDetection: null }, + focusNewInput = false, + emitEvent = true, + ) { + this.autofillOptionsForm.controls.uris.push(this.formBuilder.control(uriFieldValue), { + emitEvent, + }); if (focusNewInput) { this.focusOnNewInput$.next(); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts index e9581859b2e..39a59192985 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts @@ -20,8 +20,10 @@ describe("CardDetailsSectionComponent", () => { let registerChildFormSpy: jest.SpyInstance; let patchCipherSpy: jest.SpyInstance; + const getInitialCipherView = jest.fn(() => null); + beforeEach(async () => { - cipherFormProvider = mock(); + cipherFormProvider = mock({ getInitialCipherView }); registerChildFormSpy = jest.spyOn(cipherFormProvider, "registerChildForm"); patchCipherSpy = jest.spyOn(cipherFormProvider, "patchCipher"); @@ -94,7 +96,7 @@ describe("CardDetailsSectionComponent", () => { expect(component.cardDetailsForm.disabled).toBe(true); }); - it("initializes `cardDetailsForm` with current values", () => { + it("initializes `cardDetailsForm` from `getInitialCipherValue`", () => { const cardholderName = "Ron Burgundy"; const number = "4242 4242 4242 4242"; const code = "619"; @@ -105,9 +107,7 @@ describe("CardDetailsSectionComponent", () => { cardView.code = code; cardView.brand = "Visa"; - component.originalCipherView = { - card: cardView, - } as CipherView; + getInitialCipherView.mockReturnValueOnce({ card: cardView }); component.ngOnInit(); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index 63c5906b570..c2b3ebb59aa 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -136,8 +136,10 @@ export class CardDetailsSectionComponent implements OnInit { } ngOnInit() { - if (this.originalCipherView?.card) { - this.setInitialValues(); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + this.setInitialValues(prefillCipher); } if (this.disabled) { @@ -172,8 +174,8 @@ export class CardDetailsSectionComponent implements OnInit { } /** Set form initial form values from the current cipher */ - private setInitialValues() { - const { cardholderName, number, brand, expMonth, expYear, code } = this.originalCipherView.card; + private setInitialValues(cipherView: CipherView) { + const { cardholderName, number, brand, expMonth, expYear, code } = cipherView.card; this.cardDetailsForm.setValue({ cardholderName: cardholderName, diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 3d3e04864e8..a4f8f8f61e0 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -38,6 +38,7 @@ import { import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; import { CipherForm, CipherFormContainer } from "../cipher-form-container"; +import { CipherFormCacheService } from "../services/default-cipher-form-cache.service"; import { AdditionalOptionsSectionComponent } from "./additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component"; @@ -55,6 +56,9 @@ import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.componen provide: CipherFormContainer, useExisting: forwardRef(() => CipherFormComponent), }, + { + provide: CipherFormCacheService, + }, ], imports: [ AsyncActionsModule, @@ -164,6 +168,26 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci */ patchCipher(updateFn: (current: CipherView) => CipherView): void { this.updatedCipherView = updateFn(this.updatedCipherView); + // Cache the updated cipher + this.cipherFormCacheService.cacheCipherView(this.updatedCipherView); + } + + /** + * Return initial values for given keys of a cipher + */ + getInitialCipherView(): CipherView { + const cachedCipherView = this.cipherFormCacheService.getCachedCipherView(); + + if (cachedCipherView) { + return cachedCipherView; + } + + return this.originalCipherView; + } + + /** */ + initializedWithCachedCipher(): boolean { + return this.cipherFormCacheService.initializedWithValue; } /** @@ -187,6 +211,8 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci // Force change detection so that all child components are destroyed and re-created this.changeDetectorRef.detectChanges(); + await this.cipherFormCacheService.init(); + this.updatedCipherView = new CipherView(); this.originalCipherView = null; this.cipherForm = this.formBuilder.group({}); @@ -220,16 +246,39 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci } } + this.setInitialCipherFromCache(); + this.loading = false; this.formReadySubject.next(); } + /** + * Updates `updatedCipherView` based on the value from the cache. + */ + setInitialCipherFromCache() { + const cachedCipher = this.cipherFormCacheService.getCachedCipherView(); + if (cachedCipher === null) { + return; + } + + // Use the cached cipher when it matches the cipher being edited + if (this.updatedCipherView.id === cachedCipher.id) { + this.updatedCipherView = cachedCipher; + } + + // `id` is null when a cipher is being added + if (this.updatedCipherView.id === null) { + this.updatedCipherView = cachedCipher; + } + } + constructor( private formBuilder: FormBuilder, private addEditFormService: CipherFormService, private toastService: ToastService, private i18nService: I18nService, private changeDetectorRef: ChangeDetectorRef, + private cipherFormCacheService: CipherFormCacheService, ) {} /** diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html index 445908679c3..16bccebb939 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html @@ -2,9 +2,11 @@ *ngIf="type === 'password'" [disableMargin]="disableMargin" (onGenerated)="onCredentialGenerated($event)" + (onAlgorithm)="algorithm($event)" > diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index 1a2bac2ce50..fdde5e15d91 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -5,7 +5,7 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { GeneratorModule } from "@bitwarden/generator-components"; -import { GeneratedCredential } from "@bitwarden/generator-core"; +import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core"; /** * Renders a password or username generator UI and emits the most recently generated value. @@ -18,6 +18,9 @@ import { GeneratedCredential } from "@bitwarden/generator-core"; imports: [CommonModule, GeneratorModule], }) export class CipherFormGeneratorComponent { + @Input() + algorithm: (selected: AlgorithmInfo) => void; + /** * The type of generator form to show. */ diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts index f63a974d487..62d171b8436 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts @@ -38,6 +38,8 @@ describe("AddEditCustomFieldDialogComponent", () => { fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); component = fixture.componentInstance; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions fixture.detectChanges; }); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 63941c6a467..fab3c8f1ab1 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -46,7 +46,7 @@

{{ "customFields" | i18n }}

bitSuffix bitPasswordInputToggle data-testid="visibility-for-custom-hidden-field" - [disabled]="!canViewPasswords(i)" + *ngIf="canViewPasswords(i)" (toggledChange)="logHiddenEvent($event)" > diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts index 0c0fa1b4184..945f56821d2 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -19,6 +19,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { DialogService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "../../../../../components/src/form-field/password-input-toggle.directive"; import { CipherFormConfig } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -59,7 +61,13 @@ describe("CustomFieldsComponent", () => { }, { provide: CipherFormContainer, - useValue: { patchCipher, originalCipherView, registerChildForm: jest.fn(), config }, + useValue: { + patchCipher, + originalCipherView, + registerChildForm: jest.fn(), + config, + getInitialCipherView: jest.fn(() => originalCipherView), + }, }, { provide: LiveAnnouncer, @@ -111,7 +119,7 @@ describe("CustomFieldsComponent", () => { ]); }); - it("forbids a user to view hidden fields when the cipher `viewPassword` is false", () => { + it("when `viewPassword` is false the user cannot see the view toggle option", () => { originalCipherView.viewPassword = false; originalCipherView.fields = mockFieldViews; @@ -121,7 +129,20 @@ describe("CustomFieldsComponent", () => { const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); - expect(button.nativeElement.disabled).toBe(true); + expect(button).toBeFalsy(); + }); + + it("when `viewPassword` is true the user can see the view toggle option", () => { + originalCipherView.viewPassword = true; + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button).toBeTruthy(); }); describe("linkedFieldOptions", () => { diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index d723bad5751..f17432a993b 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -148,8 +148,10 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { value: id, })); - // Populate the form with the existing fields - this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => { + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + // When available, populate the form with the existing fields + prefillCipher.fields?.forEach((field) => { let value: string | boolean = field.value; if (field.type === FieldType.Boolean) { @@ -167,6 +169,10 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { ); }); + if (!this.cipherFormContainer.originalCipherView?.viewPassword) { + this.customFieldsForm.disable(); + } + // Disable the form if in partial-edit mode // Must happen after the initial fields are populated if (this.cipherFormContainer.config.mode === "partial-edit") { diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index b1403231ecf..2d0608bf38a 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -113,8 +113,10 @@ export class IdentitySectionComponent implements OnInit { this.identityForm.disable(); } - if (this.originalCipherView && this.originalCipherView.id) { - this.populateFormData(); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + this.populateFormData(prefillCipher); } else { this.identityForm.patchValue({ username: this.cipherFormContainer.config.initialValues?.username || "", @@ -122,8 +124,9 @@ export class IdentitySectionComponent implements OnInit { } } - populateFormData() { - const { identity } = this.originalCipherView; + populateFormData(cipherView: CipherView) { + const { identity } = cipherView; + this.identityForm.setValue({ title: identity.title, firstName: identity.firstName, diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index 32c1e7417e4..26f967e4a53 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -9,7 +9,6 @@ import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SelectComponent } from "@bitwarden/components"; @@ -25,9 +24,17 @@ describe("ItemDetailsSectionComponent", () => { let i18nService: MockProxy; const activeAccount$ = new BehaviorSubject<{ email: string }>({ email: "test@example.com" }); + const getInitialCipherView = jest.fn(() => null); + const initializedWithCachedCipher = jest.fn(() => false); beforeEach(async () => { - cipherFormProvider = mock(); + getInitialCipherView.mockClear(); + initializedWithCachedCipher.mockClear(); + + cipherFormProvider = mock({ + getInitialCipherView, + initializedWithCachedCipher, + }); i18nService = mock(); await TestBed.configureTestingModule({ @@ -95,13 +102,14 @@ describe("ItemDetailsSectionComponent", () => { readOnly: false, } as CollectionView, ]; - component.originalCipherView = { + + getInitialCipherView.mockReturnValueOnce({ name: "cipher1", organizationId: "org1", folderId: "folder1", collectionIds: ["col1"], favorite: true, - } as CipherView; + }); await component.ngOnInit(); tick(); @@ -118,55 +126,6 @@ describe("ItemDetailsSectionComponent", () => { expect(updatedCipher.favorite).toBe(true); })); - it("should prioritize initialValues when editing an existing cipher ", fakeAsync(async () => { - component.config.allowPersonalOwnership = true; - component.config.organizations = [{ id: "org1" } as Organization]; - component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - assigned: true, - readOnly: true, - } as CollectionView, - { - id: "col2", - name: "Collection 2", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, - ]; - component.originalCipherView = { - name: "cipher1", - organizationId: "org1", - folderId: "folder1", - collectionIds: ["col1"], - favorite: true, - } as CipherView; - - component.config.initialValues = { - name: "new-name", - folderId: "new-folder", - organizationId: "bad-org" as OrganizationId, // Should not be set in edit mode - collectionIds: ["col2" as CollectionId], - }; - - await component.ngOnInit(); - tick(); - - expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); - const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; - - const updatedCipher = patchFn(new CipherView()); - - expect(updatedCipher.name).toBe("new-name"); - expect(updatedCipher.organizationId).toBe("org1"); - expect(updatedCipher.folderId).toBe("new-folder"); - expect(updatedCipher.collectionIds).toEqual(["col2"]); - expect(updatedCipher.favorite).toBe(true); - })); - it("should disable organizationId control if ownership change is not allowed", async () => { component.config.allowPersonalOwnership = false; component.config.organizations = [{ id: "org1" } as Organization]; @@ -198,8 +157,11 @@ describe("ItemDetailsSectionComponent", () => { }); describe("allowOwnershipChange", () => { - it("should not allow ownership change in edit mode", () => { + it("should not allow ownership change if in edit mode and the cipher is owned by an organization", () => { component.config.mode = "edit"; + component.originalCipherView = { + organizationId: "org1", + } as CipherView; expect(component.allowOwnershipChange).toBe(false); }); @@ -236,6 +198,7 @@ describe("ItemDetailsSectionComponent", () => { it("should show personal ownership when the configuration allows", () => { component.config.mode = "edit"; component.config.allowPersonalOwnership = true; + component.originalCipherView = {} as CipherView; component.config.organizations = [{ id: "134-433-22" } as Organization]; fixture.detectChanges(); @@ -249,6 +212,7 @@ describe("ItemDetailsSectionComponent", () => { it("should show personal ownership when the control is disabled", async () => { component.config.mode = "edit"; component.config.allowPersonalOwnership = false; + component.originalCipherView = {} as CipherView; component.config.organizations = [{ id: "134-433-22" } as Organization]; await component.ngOnInit(); fixture.detectChanges(); @@ -294,10 +258,13 @@ describe("ItemDetailsSectionComponent", () => { }); describe("cloneMode", () => { - it("should append '- Clone' to the title if in clone mode", async () => { + beforeEach(() => { component.config.mode = "clone"; + }); + + it("should append '- Clone' to the title if in clone mode", async () => { component.config.allowPersonalOwnership = true; - component.originalCipherView = { + const cipher = { name: "cipher1", organizationId: null, folderId: null, @@ -305,6 +272,8 @@ describe("ItemDetailsSectionComponent", () => { favorite: false, } as CipherView; + getInitialCipherView.mockReturnValueOnce(cipher); + i18nService.t.calledWith("clone").mockReturnValue("Clone"); await component.ngOnInit(); @@ -312,8 +281,28 @@ describe("ItemDetailsSectionComponent", () => { expect(component.itemDetailsForm.controls.name.value).toBe("cipher1 - Clone"); }); + it("does not append clone when the cipher was populated from the cache", async () => { + component.config.allowPersonalOwnership = true; + const cipher = { + name: "from cache cipher", + organizationId: null, + folderId: null, + collectionIds: null, + favorite: false, + } as CipherView; + + getInitialCipherView.mockReturnValueOnce(cipher); + + initializedWithCachedCipher.mockReturnValueOnce(true); + + i18nService.t.calledWith("clone").mockReturnValue("Clone"); + + await component.ngOnInit(); + + expect(component.itemDetailsForm.controls.name.value).toBe("from cache cipher"); + }); + it("should select the first organization if personal ownership is not allowed", async () => { - component.config.mode = "clone"; component.config.allowPersonalOwnership = false; component.config.organizations = [ { id: "org1" } as Organization, @@ -376,13 +365,13 @@ describe("ItemDetailsSectionComponent", () => { it("should set collectionIds to originalCipher collections on first load", async () => { component.config.mode = "clone"; - component.originalCipherView = { + getInitialCipherView.mockReturnValueOnce({ name: "cipher1", organizationId: "org1", folderId: "folder1", collectionIds: ["col1", "col2"], favorite: true, - } as CipherView; + }); component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ { @@ -447,6 +436,13 @@ describe("ItemDetailsSectionComponent", () => { it("should show readonly hint if readonly collections are present", async () => { component.config.mode = "edit"; + getInitialCipherView.mockReturnValueOnce({ + name: "cipher1", + organizationId: "org1", + folderId: "folder1", + collectionIds: ["col1", "col2", "col3"], + favorite: true, + }); component.originalCipherView = { name: "cipher1", organizationId: "org1", @@ -559,6 +555,9 @@ describe("ItemDetailsSectionComponent", () => { collectionIds: ["col1", "col2", "col3"], favorite: true, } as CipherView; + + getInitialCipherView.mockReturnValue(component.originalCipherView); + component.config.organizations = [{ id: "org1" } as Organization]; }); diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index f7fd228232e..e6799c54cb0 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -150,8 +150,8 @@ export class ItemDetailsSectionComponent implements OnInit { } get allowOwnershipChange() { - // Do not allow ownership change in edit mode. - if (this.config.mode === "edit") { + // Do not allow ownership change in edit mode and the cipher is owned by an organization + if (this.config.mode === "edit" && this.originalCipherView.organizationId != null) { return false; } @@ -183,8 +183,10 @@ export class ItemDetailsSectionComponent implements OnInit { throw new Error("No organizations available for ownership."); } - if (this.originalCipherView) { - await this.initFromExistingCipher(); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + await this.initFromExistingCipher(prefillCipher); } else { this.itemDetailsForm.setValue({ name: this.initialValues?.name || "", @@ -210,30 +212,37 @@ export class ItemDetailsSectionComponent implements OnInit { .subscribe(); } - private async initFromExistingCipher() { + private async initFromExistingCipher(prefillCipher: CipherView) { + const { name, folderId, collectionIds } = prefillCipher; + this.itemDetailsForm.setValue({ - name: this.initialValues?.name ?? this.originalCipherView.name, - organizationId: this.originalCipherView.organizationId, // We do not allow changing ownership of an existing cipher. - folderId: this.initialValues?.folderId ?? this.originalCipherView.folderId, + name: name ? name : (this.initialValues?.name ?? ""), + organizationId: prefillCipher.organizationId, // We do not allow changing ownership of an existing cipher. + folderId: folderId ? folderId : (this.initialValues?.folderId ?? null), collectionIds: [], - favorite: this.originalCipherView.favorite, + favorite: prefillCipher.favorite, }); + const initializedWithCachedCipher = this.cipherFormContainer.initializedWithCachedCipher(); + // Configure form for clone mode. if (this.config.mode === "clone") { - this.itemDetailsForm.controls.name.setValue( - this.originalCipherView.name + " - " + this.i18nService.t("clone"), - ); + if (!initializedWithCachedCipher) { + this.itemDetailsForm.controls.name.setValue( + prefillCipher.name + " - " + this.i18nService.t("clone"), + ); + } - if (!this.allowPersonalOwnership && this.originalCipherView.organizationId == null) { + if (!this.allowPersonalOwnership && prefillCipher.organizationId == null) { this.itemDetailsForm.controls.organizationId.setValue(this.defaultOwner); } } - await this.updateCollectionOptions( - this.initialValues?.collectionIds ?? - (this.originalCipherView.collectionIds as CollectionId[]), - ); + const prefillCollections = collectionIds?.length + ? (collectionIds as CollectionId[]) + : (this.initialValues?.collectionIds ?? []); + + await this.updateCollectionOptions(prefillCollections); if (this.partialEdit) { this.itemDetailsForm.disable(); diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index 182427f7f42..453b0dce42d 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -14,6 +14,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service"; @@ -43,9 +45,11 @@ describe("LoginDetailsSectionComponent", () => { let configService: MockProxy; const collect = jest.fn().mockResolvedValue(null); + const getInitialCipherView = jest.fn(() => null); beforeEach(async () => { - cipherFormContainer = mock(); + getInitialCipherView.mockClear(); + cipherFormContainer = mock({ getInitialCipherView }); generationService = mock(); auditService = mock(); @@ -120,18 +124,18 @@ describe("LoginDetailsSectionComponent", () => { }); it("initializes 'loginDetailsForm' with original cipher view values", async () => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValueOnce({ viewPassword: true, login: { password: "original-password", username: "original-username", totp: "original-totp", - } as LoginView, - } as CipherView; + }, + }); - await component.ngOnInit(); + component.ngOnInit(); - expect(component.loginDetailsForm.value).toEqual({ + expect(component.loginDetailsForm.getRawValue()).toEqual({ username: "original-username", password: "original-password", totp: "original-totp", @@ -139,22 +143,23 @@ describe("LoginDetailsSectionComponent", () => { }); it("initializes 'loginDetailsForm' with initialValues that override any original cipher view values", async () => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValueOnce({ viewPassword: true, login: { password: "original-password", username: "original-username", totp: "original-totp", - } as LoginView, - } as CipherView; + }, + }); + cipherFormContainer.config.initialValues = { username: "new-username", password: "new-password", }; - await component.ngOnInit(); + component.ngOnInit(); - expect(component.loginDetailsForm.value).toEqual({ + expect(component.loginDetailsForm.getRawValue()).toEqual({ username: "new-username", password: "new-password", totp: "original-totp", @@ -163,12 +168,12 @@ describe("LoginDetailsSectionComponent", () => { describe("viewHiddenFields", () => { beforeEach(() => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValue({ viewPassword: false, login: { password: "original-password", - } as LoginView, - } as CipherView; + }, + }); }); it("returns value of originalCipher.viewPassword", () => { @@ -249,6 +254,10 @@ describe("LoginDetailsSectionComponent", () => { }); describe("password", () => { + beforeEach(() => { + getInitialCipherView.mockReturnValue(null); + }); + const getGeneratePasswordBtn = () => fixture.nativeElement.querySelector("button[data-testid='generate-password-button']"); @@ -518,11 +527,11 @@ describe("LoginDetailsSectionComponent", () => { fixture.nativeElement.querySelector("input[data-testid='passkey-field']"); beforeEach(() => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValue({ login: Object.assign(new LoginView(), { fido2Credentials: [{ creationDate: passkeyDate } as Fido2CredentialView], }), - } as CipherView; + }); fixture = TestBed.createComponent(LoginDetailsSectionComponent); component = fixture.componentInstance; @@ -565,7 +574,11 @@ describe("LoginDetailsSectionComponent", () => { }); it("hides the passkey field when missing a passkey", () => { - (cipherFormContainer.originalCipherView as CipherView).login.fido2Credentials = []; + getInitialCipherView.mockReturnValueOnce({ + login: Object.assign(new LoginView(), { + fido2Credentials: [], + }), + }); fixture.detectChanges(); diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 2296932aca3..92def705852 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -139,11 +139,13 @@ export class LoginDetailsSectionComponent implements OnInit { }); } - async ngOnInit() { - if (this.cipherFormContainer.originalCipherView?.login) { - this.initFromExistingCipher(this.cipherFormContainer.originalCipherView.login); + ngOnInit() { + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + this.initFromExistingCipher(prefillCipher.login); } else { - await this.initNewCipher(); + this.initNewCipher(); } if (this.cipherFormContainer.config.mode === "partial-edit") { @@ -166,7 +168,7 @@ export class LoginDetailsSectionComponent implements OnInit { } } - private async initNewCipher() { + private initNewCipher() { this.loginDetailsForm.patchValue({ username: this.initialValues?.username || "", password: this.initialValues?.password || "", diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts new file mode 100644 index 00000000000..6236e2d3dac --- /dev/null +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts @@ -0,0 +1,97 @@ +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { CipherFormCacheService } from "./default-cipher-form-cache.service"; + +describe("CipherFormCacheService", () => { + let service: CipherFormCacheService; + let testBed: TestBed; + const cacheSignal = signal(null); + const getCacheSignal = jest.fn().mockReturnValue(cacheSignal); + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const cacheSetMock = jest.spyOn(cacheSignal, "set"); + + beforeEach(() => { + getCacheSignal.mockClear(); + getFeatureFlag.mockClear(); + cacheSetMock.mockClear(); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: ViewCacheService, useValue: { signal: getCacheSignal } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + CipherFormCacheService, + ], + }); + }); + + describe("feature enabled", () => { + beforeEach(async () => { + getFeatureFlag.mockResolvedValue(true); + }); + + it("`getCachedCipherView` returns the cipher", async () => { + cacheSignal.set({ id: "cipher-4" } as CipherView); + service = testBed.inject(CipherFormCacheService); + await service.init(); + + expect(service.getCachedCipherView()).toEqual({ id: "cipher-4" }); + }); + + it("updates the signal value", async () => { + service = testBed.inject(CipherFormCacheService); + await service.init(); + + service.cacheCipherView({ id: "cipher-5" } as CipherView); + + expect(cacheSignal.set).toHaveBeenCalledWith({ id: "cipher-5" }); + }); + + describe("initializedWithValue", () => { + it("sets `initializedWithValue` to true when there is a cached cipher", async () => { + cacheSignal.set({ id: "cipher-3" } as CipherView); + service = testBed.inject(CipherFormCacheService); + await service.init(); + + expect(service.initializedWithValue).toBe(true); + }); + + it("sets `initializedWithValue` to false when there is not a cached cipher", async () => { + cacheSignal.set(null); + service = testBed.inject(CipherFormCacheService); + await service.init(); + + expect(service.initializedWithValue).toBe(false); + }); + }); + }); + + describe("featured disabled", () => { + beforeEach(async () => { + cacheSignal.set({ id: "cipher-1" } as CipherView); + getFeatureFlag.mockResolvedValue(false); + cacheSetMock.mockClear(); + + service = testBed.inject(CipherFormCacheService); + await service.init(); + }); + + it("sets `initializedWithValue` to false", () => { + expect(service.initializedWithValue).toBe(false); + }); + + it("`getCachedCipherView` returns null", () => { + expect(service.getCachedCipherView()).toBeNull(); + }); + + it("does not update the signal value", () => { + service.cacheCipherView({ id: "cipher-2" } as CipherView); + + expect(cacheSignal.set).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts new file mode 100644 index 00000000000..268b2db306b --- /dev/null +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -0,0 +1,73 @@ +import { inject, Injectable } from "@angular/core"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +const CIPHER_FORM_CACHE_KEY = "cipher-form-cache"; + +@Injectable() +export class CipherFormCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the `PM9111ExtensionPersistAddEditForm` flag is enabled */ + private featureEnabled: boolean = false; + + /** + * When true the `CipherFormCacheService` a cipher was stored in cache when the service was initialized. + * Otherwise false, when the cache was empty. + * + * This is helpful to know the initial state of the cache as it can be populated quickly after initialization. + */ + initializedWithValue: boolean; + + private cipherCache = this.viewCacheService.signal({ + key: CIPHER_FORM_CACHE_KEY, + initialValue: null, + deserializer: CipherView.fromJSON, + }); + + constructor() { + this.initializedWithValue = !!this.cipherCache(); + } + + /** + * Must be called once before interacting with the cached cipher, otherwise methods will be noop. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9111ExtensionPersistAddEditForm, + ); + + if (!this.featureEnabled) { + this.initializedWithValue = false; + } + } + + /** + * Update the cache with the new CipherView. + */ + cacheCipherView(cipherView: CipherView): void { + if (!this.featureEnabled) { + return; + } + + // Create a new shallow reference to force the signal to update + // By default, signals use `Object.is` to determine equality + // Docs: https://angular.dev/guide/signals#signal-equality-functions + this.cipherCache.set({ ...cipherView } as CipherView); + } + + /** + * Returns the cached CipherView when available. + */ + getCachedCipherView(): CipherView | null { + if (!this.featureEnabled) { + return null; + } + + return this.cipherCache(); + } +} diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 93a53345d3a..28b13b51c61 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -8,7 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, 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"; @@ -46,7 +46,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { const [organizations, collections, allowPersonalOwnership, folders, cipher] = await firstValueFrom( combineLatest([ - this.organizations$, + this.organizations$(activeUserId), this.collectionService.encryptedCollections$.pipe( switchMap((c) => this.collectionService.decryptedCollections$.pipe( @@ -78,13 +78,17 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { }; } - private organizations$ = this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter( - (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, - ), - ), - ); + organizations$(userId: UserId) { + return this.organizationService + .organizations$(userId) + .pipe( + map((orgs) => + orgs.filter( + (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, + ), + ), + ); + } private allowPersonalOwnership$ = this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 92a28f9b15f..059214cc185 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -63,8 +63,16 @@ export class DefaultCipherFormService implements CipherFormService { const originalCollectionIds = new Set(config.originalCipher.collectionIds ?? []); const newCollectionIds = new Set(cipher.collectionIds ?? []); - // If the collectionIds are the same, update the cipher normally - if (isSetEqual(originalCollectionIds, newCollectionIds)) { + // Call shareWithServer if the owner is changing from a user to an organization + if (config.originalCipher.organizationId === null && cipher.organizationId != null) { + savedCipher = await this.cipherService.shareWithServer( + cipher, + cipher.organizationId, + cipher.collectionIds, + activeUserId, + ); + // If the collectionIds are the same, update the cipher normally + } else if (isSetEqual(originalCollectionIds, newCollectionIds)) { savedCipher = await this.cipherService.updateWithServer(encryptedCipher, config.admin); } else { // Updating a cipher with collection changes is not supported with a single request currently diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 4bd87a7869d..425afeace84 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -4,9 +4,13 @@ import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { CollectionId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -101,8 +105,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy { return false; } - const { username, password, totp } = this.cipher.login; - return username || password || totp; + const { username, password, totp, fido2Credentials } = this.cipher.login; + return username || password || totp || fido2Credentials; } get hasAutofill() { @@ -133,9 +137,12 @@ export class CipherViewComponent implements OnChanges, OnDestroy { ); } - if (this.cipher.organizationId) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (this.cipher.organizationId && userId) { this.organization$ = this.organizationService - .get$(this.cipher.organizationId) + .organizations$(userId) + .pipe(getOrganizationById(this.cipher.organizationId)) .pipe(takeUntil(this.destroyed$)); } diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index 45ddc3c1dea..ab31ede57bb 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -38,6 +38,7 @@

{{ "customFields" | i18n }}

type="button" bitIconButton bitPasswordInputToggle + *ngIf="canViewPassword" (toggledChange)="logHiddenEvent($event)" >

[appCopyClick]="field.value" showToast [valueLabel]="field.name" + *ngIf="canViewPassword" [appA11yTitle]="'copyCustomField' | i18n: field.name" (click)="logCopyEvent()" > diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index b41b351e197..313607ce3a6 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -59,6 +59,10 @@ export class CustomFieldV2Component implements OnInit { return this.i18nService.t(linkedType.i18nKey); } + get canViewPassword() { + return this.cipher.viewPassword; + } + async logHiddenEvent(hiddenFieldVisible: boolean) { if (hiddenFieldVisible) { await this.eventCollectionService.collect( diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 8503604bf7c..b4a0d4841f8 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -116,7 +116,7 @@

{{ "loginCredentials" | i18n }}

{{ "verificationCodeTotp" | i18n }} {{ "loginCredentials" | i18n }} id="totp" readonly bitInput - [type]="!(isPremium$ | async) ? 'password' : 'text'" + [type]="!(allowTotpGeneration$ | async) ? 'password' : 'text'" [value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'" aria-readonly="true" data-testid="login-totp" class="tw-font-mono" />
{{ "loginCredentials" | i18n }} showToast [appA11yTitle]="'copyVerificationCode' | i18n" data-testid="copy-totp" - [disabled]="!(isPremium$ | async)" + [disabled]="!(allowTotpGeneration$ | async)" class="disabled:tw-cursor-default" > diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts index 9c09028650e..39fcc39bf58 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts @@ -4,7 +4,6 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { CopyClickDirective } from "@bitwarden/angular/directives/copy-click.directive"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; @@ -17,8 +16,12 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { BitFormFieldComponent, ToastService } from "@bitwarden/components"; +import { CopyClickDirective, BitFormFieldComponent, ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; import { LoginCredentialsViewComponent } from "./login-credentials-view.component"; diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index b24fcdfa1fd..281e187f78b 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -2,7 +2,15 @@ // @ts-strict-ignore import { CommonModule, DatePipe } from "@angular/common"; import { Component, inject, Input } from "@angular/core"; -import { Observable, switchMap } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + filter, + map, + Observable, + shareReplay, + switchMap, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -12,15 +20,17 @@ import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { + BadgeModule, + ColorPasswordModule, FormFieldModule, + IconButtonModule, SectionComponent, SectionHeaderComponent, TypographyModule, - IconButtonModule, - BadgeModule, - ColorPasswordModule, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PremiumUpgradePromptService } from "../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component"; import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; @@ -49,13 +59,31 @@ type TotpCodeValues = { ], }) export class LoginCredentialsViewComponent { - @Input() cipher: CipherView; + @Input() + get cipher(): CipherView { + return this._cipher$.value; + } + set cipher(value: CipherView) { + this._cipher$.next(value); + } + private _cipher$ = new BehaviorSubject(null); - isPremium$: Observable = this.accountService.activeAccount$.pipe( + private _userHasPremium$: Observable = this.accountService.activeAccount$.pipe( switchMap((account) => this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ), ); + + allowTotpGeneration$: Observable = combineLatest([ + this._userHasPremium$, + this._cipher$.pipe(filter((c) => c != null)), + ]).pipe( + map(([userHasPremium, cipher]) => { + // User premium status only applies to personal ciphers, organizationUseTotp applies to organization ciphers + return (userHasPremium && cipher.organizationId == null) || cipher.organizationUseTotp; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); showPasswordCount: boolean = false; passwordRevealed: boolean = false; totpCodeCopyObj: TotpCodeValues; diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts index 4c84b2852a8..21f82234b2c 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts @@ -8,6 +8,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { SectionHeaderComponent } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitInputDirective } from "../../../../components/src/input/input.directive"; import { ViewIdentitySectionsComponent } from "./view-identity-sections.component"; diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 00852ff101c..c950187edb8 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -26,10 +26,14 @@ import { import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType } 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -128,28 +132,31 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI protected orgName: string; protected showOrgSelector: boolean = false; - protected organizations$: Observable = - this.organizationService.organizations$.pipe( - map((orgs) => - orgs - .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) - .sort((a, b) => a.name.localeCompare(b.name)), + protected organizations$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService.organizations$(account?.id).pipe( + map((orgs) => + orgs + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) + .sort((a, b) => a.name.localeCompare(b.name)), + ), + tap((orgs) => { + if (orgs.length > 0 && this.showOrgSelector) { + // Using setTimeout to defer the patchValue call until the next event loop cycle + setTimeout(() => { + this.formGroup.patchValue({ selectedOrg: orgs[0].id }); + this.setFormValidators(); + + // Disable the org selector if there is only one organization + if (orgs.length === 1) { + this.formGroup.controls.selectedOrg.disable(); + } + }); + } + }), ), - tap((orgs) => { - if (orgs.length > 0 && this.showOrgSelector) { - // Using setTimeout to defer the patchValue call until the next event loop cycle - setTimeout(() => { - this.formGroup.patchValue({ selectedOrg: orgs[0].id }); - this.setFormValidators(); - - // Disable the org selector if there is only one organization - if (orgs.length === 1) { - this.formGroup.controls.selectedOrg.disable(); - } - }); - } - }), - ); + ), + ); protected transferWarningText = (orgName: string, itemsCount: number) => { const haveOrgName = !!orgName; @@ -354,7 +361,10 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI return; } - const org = await this.organizationService.get(organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const org = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), + ); this.orgName = org.name; this.editableItems = org.canEditAllCiphers @@ -408,7 +418,9 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI private getCollectionsForOrganization(orgId: OrganizationId): Observable { return combineLatest([ this.collectionService.decryptedCollections$, - this.organizationService.organizations$, + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ), ]).pipe( map(([collections, organizations]) => { const org = organizations.find((o) => o.id === orgId); diff --git a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.html b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.html new file mode 100644 index 00000000000..824b16bb3ac --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.html @@ -0,0 +1,13 @@ + diff --git a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.spec.ts b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.spec.ts new file mode 100644 index 00000000000..91239cd7520 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.spec.ts @@ -0,0 +1,71 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { VaultCarouselSlideComponent } from "../carousel-slide/carousel-slide.component"; + +import { VaultCarouselButtonComponent } from "./carousel-button.component"; + +describe("VaultCarouselButtonComponent", () => { + let fixture: ComponentFixture; + let component: VaultCarouselButtonComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselButtonComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VaultCarouselButtonComponent); + component = fixture.componentInstance; + component.slide = { label: "Test Label" } as VaultCarouselSlideComponent; + + fixture.detectChanges(); + }); + + it("emits click event", () => { + jest.spyOn(component.onClick, "emit"); + component.button.nativeElement.click(); + + expect(component.onClick.emit).toHaveBeenCalled(); + }); + + it("focuses on button", () => { + component.focus(); + + expect(document.activeElement).toBe(component.button.nativeElement); + }); + + it('sets the "aria-label" attribute', () => { + expect(component.button.nativeElement.getAttribute("aria-label")).toBe("Test Label"); + }); + + describe("is active", () => { + beforeEach(() => { + component.isActive = true; + fixture.detectChanges(); + }); + + it("sets the aria-selected to true", () => { + expect(component.button.nativeElement.getAttribute("aria-selected")).toBe("true"); + }); + + it("adds button to tab index", () => { + expect(component.button.nativeElement.getAttribute("tabindex")).toBe("0"); + }); + }); + + describe("is not active", () => { + beforeEach(() => { + component.isActive = false; + fixture.detectChanges(); + }); + + it("sets the aria-selected to false", () => { + expect(component.button.nativeElement.getAttribute("aria-selected")).toBe("false"); + }); + + it("removes button from tab index", () => { + expect(component.button.nativeElement.getAttribute("tabindex")).toBe("-1"); + }); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts new file mode 100644 index 00000000000..d0e353dcc76 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts @@ -0,0 +1,45 @@ +import { FocusableOption } from "@angular/cdk/a11y"; +import { CommonModule } from "@angular/common"; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from "@angular/core"; + +import { IconModule } from "@bitwarden/components"; + +import { CarouselIcon } from "../carousel-icons/carousel-icon"; +import { VaultCarouselSlideComponent } from "../carousel-slide/carousel-slide.component"; + +@Component({ + selector: "vault-carousel-button", + templateUrl: "carousel-button.component.html", + standalone: true, + imports: [CommonModule, IconModule], +}) +export class VaultCarouselButtonComponent implements FocusableOption { + /** Slide component that is associated with the individual button */ + @Input({ required: true }) slide!: VaultCarouselSlideComponent; + + @ViewChild("btn", { static: true }) button!: ElementRef; + protected CarouselIcon = CarouselIcon; + + /** When set to true the button is shown in an active state. */ + @Input({ required: true }) isActive!: boolean; + + /** Emits when the button is clicked. */ + @Output() onClick = new EventEmitter(); + + /** Focuses the underlying button element. */ + focus(): void { + this.button.nativeElement.focus(); + } + + protected get dynamicClasses(): string[] { + const activeClasses = ["[&_rect]:tw-fill-primary-600", "tw-text-primary-600"]; + + const inactiveClasses = [ + "tw-text-muted", + "[&_rect]:hover:tw-fill-text-muted", + "focus-visible:tw-text-info-700", + ]; + + return this.isActive ? activeClasses : inactiveClasses; + } +} diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.html b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.html new file mode 100644 index 00000000000..4d97cfd054f --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts new file mode 100644 index 00000000000..2900225b886 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts @@ -0,0 +1,58 @@ +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { VaultCarouselContentComponent } from "./carousel-content.component"; + +@Component({ + selector: "app-test-template-ref", + standalone: true, + imports: [VaultCarouselContentComponent], + template: ` + +

Test Template Content

+
+ + `, +}) +class TestTemplateRefComponent implements OnInit { + // Test template content by creating a wrapping component and then pass a portal to the carousel content component. + @ViewChild("template", { static: true }) template!: TemplateRef; + portal!: TemplatePortal; + + constructor(private viewContainerRef: ViewContainerRef) {} + + ngOnInit() { + this.portal = new TemplatePortal(this.template, this.viewContainerRef); + } +} + +describe("VaultCarouselContentComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselContentComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestTemplateRefComponent); + fixture.detectChanges(); + }); + + it("displays content", () => { + const carouselContent = fixture.debugElement.query(By.directive(VaultCarouselContentComponent)); + + expect(carouselContent.nativeElement.textContent).toBe("Test Template Content"); + }); + + it("sets aria attributes for screen readers", () => { + const carouselContent = fixture.debugElement.query(By.directive(VaultCarouselContentComponent)); + const wrappingDiv = carouselContent.nativeElement.querySelector("div"); + + expect(wrappingDiv.getAttribute("aria-live")).toBe("polite"); + expect(wrappingDiv.getAttribute("aria-atomic")).toBe("false"); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts new file mode 100644 index 00000000000..7051a8ff8fa --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts @@ -0,0 +1,13 @@ +import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal"; +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "vault-carousel-content", + templateUrl: "carousel-content.component.html", + standalone: true, + imports: [CdkPortalOutlet], +}) +export class VaultCarouselContentComponent { + /** Content to be displayed for the carousel. */ + @Input({ required: true }) content!: TemplatePortal; +} diff --git a/libs/vault/src/components/carousel/carousel-icons/carousel-icon.ts b/libs/vault/src/components/carousel/carousel-icons/carousel-icon.ts new file mode 100644 index 00000000000..64052e6fa6c --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-icons/carousel-icon.ts @@ -0,0 +1,7 @@ +import { svgIcon } from "@bitwarden/components"; + +export const CarouselIcon = svgIcon` + + + +`; diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.html b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.html new file mode 100644 index 00000000000..0c5c6f1cd70 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.html @@ -0,0 +1,9 @@ + +
+ +
+
diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts new file mode 100644 index 00000000000..e69a2a2d758 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts @@ -0,0 +1,40 @@ +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { VaultCarouselSlideComponent } from "./carousel-slide.component"; + +@Component({ + selector: "app-test-carousel-slide", + standalone: true, + imports: [VaultCarouselSlideComponent], + template: `

Carousel Slide Content!

`, +}) +class TestCarouselSlideComponent { + // Test template content by creating a wrapping component. +} + +describe("VaultCarouselSlideComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselSlideComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestCarouselSlideComponent); + fixture.detectChanges(); + }); + + it("sets content", () => { + const slideComponent = fixture.debugElement.query( + By.directive(VaultCarouselSlideComponent), + ).componentInstance; + + expect(slideComponent.content).not.toBeNull(); + expect(slideComponent.content).toBeInstanceOf(TemplatePortal); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts new file mode 100644 index 00000000000..23ce6a3d7db --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts @@ -0,0 +1,41 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; + +@Component({ + selector: "vault-carousel-slide", + templateUrl: "./carousel-slide.component.html", + standalone: true, +}) +export class VaultCarouselSlideComponent implements OnInit { + /** `aria-label` that is assigned to the carousel toggle. */ + @Input({ required: true }) label!: string; + + /** + * Should be set to true when the slide has no focusable elements. + * + * When the slide does not contain any focusable elements or the first element with content is not focusable, + * this should be set to 0 to include it in the tab sequence of the page. + * + * @remarks See note 4 of https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ + */ + @Input({ transform: coerceBooleanProperty }) noFocusableChildren?: true; + + @ViewChild(TemplateRef, { static: true }) implicitContent!: TemplateRef; + + private _contentPortal: TemplatePortal | null = null; + + /** + * A Portal containing the content of the slide. + * Used by `VaultCarouselComponent` when the slide becomes active. + */ + get content(): TemplatePortal | null { + return this._contentPortal; + } + + constructor(private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + this._contentPortal = new TemplatePortal(this.implicitContent, this.viewContainerRef); + } +} diff --git a/libs/vault/src/components/carousel/carousel.component.html b/libs/vault/src/components/carousel/carousel.component.html new file mode 100644 index 00000000000..b3e124e02bb --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.component.html @@ -0,0 +1,25 @@ +
+ +
+ +
+
+ +
+
diff --git a/libs/vault/src/components/carousel/carousel.component.spec.ts b/libs/vault/src/components/carousel/carousel.component.spec.ts new file mode 100644 index 00000000000..500b8115bad --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.component.spec.ts @@ -0,0 +1,73 @@ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; +import { VaultCarouselComponent } from "./carousel.component"; + +@Component({ + selector: "app-test-carousel-slide", + standalone: true, + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + template: ` + + +

First Carousel Heading

+
+ +

Second Carousel Heading

+
+ +

Third Carousel Heading

+
+
+ `, +}) +class TestCarouselComponent { + // Test carousel by creating a wrapping component. +} + +describe("VaultCarouselComponent", () => { + let fixture: ComponentFixture; + let component: VaultCarouselComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestCarouselComponent); + fixture.detectChanges(); + component = fixture.debugElement.query(By.directive(VaultCarouselComponent)).componentInstance; + }); + + it("sets first slide as active by default", () => { + expect(component["selectedIndex"]).toBe(0); + }); + + it("shows the active slides content", () => { + // Set the second slide as active + fixture.debugElement.queryAll(By.css("button"))[1].nativeElement.click(); + fixture.detectChanges(); + + const heading = fixture.debugElement.query(By.css("h1")).nativeElement; + + expect(heading.textContent).toBe("Second Carousel Heading"); + }); + + it("sets the initial focused button as the first button", () => { + expect(component["keyManager"]?.activeItemIndex).toBe(0); + }); + + it('emits "slideChange" event when slide changes', () => { + jest.spyOn(component.slideChange, "emit"); + + const thirdSlideButton = fixture.debugElement.queryAll(By.css("button"))[2]; + + thirdSlideButton.nativeElement.click(); + + expect(component.slideChange.emit).toHaveBeenCalledWith(2); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts new file mode 100644 index 00000000000..ab6d0a38f3e --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -0,0 +1,145 @@ +import { FocusKeyManager } from "@angular/cdk/a11y"; +import { CdkPortalOutlet } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + Input, + Output, + QueryList, + ViewChild, + ViewChildren, + inject, +} from "@angular/core"; + +import { ButtonModule } from "@bitwarden/components"; + +import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button.component"; +import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component"; +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; + +@Component({ + selector: "vault-carousel", + templateUrl: "./carousel.component.html", + standalone: true, + imports: [ + CdkPortalOutlet, + CommonModule, + ButtonModule, + VaultCarouselContentComponent, + VaultCarouselButtonComponent, + ], +}) +export class VaultCarouselComponent implements AfterViewInit { + private changeDetectorRef = inject(ChangeDetectorRef); + /** + * Accessible Label for the carousel + * + * @remarks + * The label should not include the word "carousel", `aria-roledescription="carousel"` is already included. + */ + @Input({ required: true }) label = ""; + + /** + * Emits the index of of the newly selected slide. + */ + @Output() slideChange = new EventEmitter(); + + /** All slides within the carousel. */ + @ContentChildren(VaultCarouselSlideComponent) slides!: QueryList; + + /** All buttons that control the carousel */ + @ViewChildren(VaultCarouselButtonComponent) + carouselButtons!: QueryList; + + /** Wrapping container for the carousel content and buttons */ + @ViewChild("container") carouselContainer!: ElementRef; + + /** Container for the carousel buttons */ + @ViewChild("carouselButtonWrapper") carouselButtonWrapper!: ElementRef; + + /** Temporary container containing `tempSlideOutlet` */ + @ViewChild("tempSlideContainer") tempSlideContainer!: ElementRef; + + /** Outlet to temporary render each slide within */ + @ViewChild(CdkPortalOutlet) tempSlideOutlet!: CdkPortalOutlet; + + /** The currently selected index of the carousel. */ + protected selectedIndex = 0; + + /** + * Slides that have differing heights can cause the carousel controls to jump. + * Set the min height based on the tallest slide. + */ + protected minHeight: `${number}px` | null = null; + + /** + * Focus key manager for keeping tab controls accessible. + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions + */ + protected keyManager: FocusKeyManager | null = null; + + /** Set the selected index of the carousel. */ + protected selectSlide(index: number) { + this.selectedIndex = index; + this.slideChange.emit(index); + } + + ngAfterViewInit(): void { + this.keyManager = new FocusKeyManager(this.carouselButtons) + .withHorizontalOrientation("ltr") + .withWrap() + .withHomeAndEnd(); + + // Set the first carousel button as active, this avoids having to double tab the arrow keys on initial focus. + this.keyManager.setFirstItemActive(); + + this.setMinHeightOfCarousel(); + } + + /** + * Slides of differing height can cause the carousel to jump in height. + * Render each slide in a temporary portal outlet to get the height of each slide + * and store the tallest slide height. + */ + private setMinHeightOfCarousel() { + // Store the height of the carousel button element. + const heightOfButtonsPx = this.carouselButtonWrapper.nativeElement.offsetHeight; + + // Get the width of the carousel so we know how much space each slide can render within. + const containerWidth = this.carouselContainer.nativeElement.offsetWidth; + const containerHeight = this.carouselContainer.nativeElement.offsetHeight; + + // Set the width of the temp container to render each slide inside of. + this.tempSlideContainer.nativeElement.style.width = `${containerWidth}px`; + + // The first slide is already rendered at this point, use the height of the container + // to determine the height of the first slide. + let tallestSlideHeightPx = containerHeight - heightOfButtonsPx; + + this.slides.forEach((slide, index) => { + // Skip the first slide, the height is accounted for above. + if (index === this.selectedIndex) { + return; + } + + this.tempSlideOutlet.attach(slide.content); + + // Store the height of the current slide if is larger than the current stored height; + if (this.tempSlideContainer.nativeElement.offsetHeight > tallestSlideHeightPx) { + tallestSlideHeightPx = this.tempSlideContainer.nativeElement.offsetHeight; + } + + // cleanup the outlet + this.tempSlideOutlet.detach(); + }); + + // Set the min height of the entire carousel based on the largest slide. + this.minHeight = `${tallestSlideHeightPx + heightOfButtonsPx}px`; + this.changeDetectorRef.detectChanges(); + } +} diff --git a/libs/vault/src/components/carousel/carousel.stories.ts b/libs/vault/src/components/carousel/carousel.stories.ts new file mode 100644 index 00000000000..521a561a19f --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.stories.ts @@ -0,0 +1,76 @@ +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; + +import { ButtonComponent, TypographyModule } from "@bitwarden/components"; + +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; +import { VaultCarouselComponent } from "./carousel.component"; + +export default { + title: "Vault/Carousel", + component: VaultCarouselComponent, + decorators: [ + moduleMetadata({ + imports: [VaultCarouselSlideComponent, TypographyModule, ButtonComponent], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) => ({ + props: args, + template: ` + + +
+

First Carousel Heading

+

First Carousel Content

+
+
+ +
+

Second Carousel Heading

+

Second Carousel Content

+
+
+ +
+

Third Carousel Heading

+

Third Carousel Content

+

Third Carousel Content

+

Third Carousel Content

+
+
+ +
+

Fourth Carousel Heading

+

Fourth Carousel Content

+
+
+
+ `, + }), +}; + +export const KeyboardNavigation: Story = { + render: (args: any) => ({ + props: args, + template: ` + + +
+

First Carousel Heading

+ +
+
+ +
+

Second Carousel Heading

+

With no focusable elements, the entire slide should be focusable

+
+
+
+ `, + }), +}; diff --git a/libs/vault/src/components/carousel/index.ts b/libs/vault/src/components/carousel/index.ts new file mode 100644 index 00000000000..a785c261020 --- /dev/null +++ b/libs/vault/src/components/carousel/index.ts @@ -0,0 +1 @@ +export { VaultCarouselComponent } from "./carousel.component"; diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html new file mode 100644 index 00000000000..4a0d60d20e3 --- /dev/null +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html @@ -0,0 +1,32 @@ + + + {{ "decryptionError" | i18n }} +
+

+ {{ "couldNotDecryptVaultItemsBelow" | i18n }} + {{ + "contactCSToAvoidDataLossPart1" | i18n + }} + {{ "contactCSToAvoidDataLossPart2" | i18n }} +

+
    +
  • + {{ id }} +
  • +
+
+ + + + diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts new file mode 100644 index 00000000000..c183c4bb246 --- /dev/null +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -0,0 +1,59 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { + AnchorLinkDirective, + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogService, + TypographyModule, +} from "@bitwarden/components"; + +export type DecryptionFailureDialogParams = { + cipherIds: CipherId[]; +}; + +@Component({ + standalone: true, + selector: "vault-decryption-failure-dialog", + templateUrl: "./decryption-failure-dialog.component.html", + imports: [ + DialogModule, + CommonModule, + TypographyModule, + JslibModule, + AsyncActionsModule, + ButtonModule, + AnchorLinkDirective, + ], +}) +export class DecryptionFailureDialogComponent { + protected dialogRef = inject(DialogRef); + protected params = inject(DIALOG_DATA); + protected platformUtilsService = inject(PlatformUtilsService); + + selectText(element: HTMLElement) { + const selection = window.getSelection(); + if (selection == null) { + return; + } + selection.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(element); + selection.addRange(range); + } + + openContactSupport(event: Event) { + event.preventDefault(); + this.platformUtilsService.launchUri("https://bitwarden.com/contact"); + } + + static open(dialogService: DialogService, params: DecryptionFailureDialogParams) { + return dialogService.open(DecryptionFailureDialogComponent, { data: params }); + } +} diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index b22ced3d899..a47cdc8d1d5 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -98,6 +98,8 @@ export class DownloadAttachmentComponent { fileName: this.attachment.fileName, blobData: decBuf, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.toastService.showToast({ variant: "error", diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts index 461cc734d76..fb90df4d6ac 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -10,6 +10,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ColorPasswordModule, ItemModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; import { PasswordHistoryViewComponent } from "./password-history-view.component"; diff --git a/libs/vault/src/components/password-reprompt.component.ts b/libs/vault/src/components/password-reprompt.component.ts index abefac58747..dc75d50c93e 100644 --- a/libs/vault/src/components/password-reprompt.component.ts +++ b/libs/vault/src/components/password-reprompt.component.ts @@ -1,10 +1,11 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Component } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -13,6 +14,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -45,6 +47,7 @@ export class PasswordRepromptComponent { protected i18nService: I18nService, protected formBuilder: FormBuilder, protected dialogRef: DialogRef, + private toastService: ToastService, protected accountService: AccountService, ) {} @@ -55,7 +58,7 @@ export class PasswordRepromptComponent { return; } - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (userId == null) { throw new Error("An active user is expected while doing password reprompt."); @@ -72,11 +75,11 @@ export class PasswordRepromptComponent { userId, )) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); return; } diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 0112de44241..23143fa2309 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -16,5 +16,8 @@ export { DownloadAttachmentComponent } from "./components/download-attachment/do export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; +export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component"; export * as VaultIcons from "./icons"; + +export * from "./tasks"; diff --git a/libs/vault/src/services/password-reprompt.service.spec.ts b/libs/vault/src/services/password-reprompt.service.spec.ts index 300b98215c2..7d0a834b2d6 100644 --- a/libs/vault/src/services/password-reprompt.service.spec.ts +++ b/libs/vault/src/services/password-reprompt.service.spec.ts @@ -1,5 +1,7 @@ import { MockProxy, mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserVerificationService } from "@bitwarden/common/src/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DialogService } from "@bitwarden/components"; diff --git a/libs/vault/src/tasks/abstractions/task.service.ts b/libs/vault/src/tasks/abstractions/task.service.ts new file mode 100644 index 00000000000..3d6ae72900f --- /dev/null +++ b/libs/vault/src/tasks/abstractions/task.service.ts @@ -0,0 +1,45 @@ +import { Observable } from "rxjs"; + +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { SecurityTask } from "@bitwarden/vault"; + +export abstract class TaskService { + /** + * Observable indicating if tasks are enabled for a given user. + * + * @remarks Internally, this checks the user's organization details to determine if tasks are enabled. + * @param userId + */ + abstract tasksEnabled$(userId: UserId): Observable; + + /** + * Observable of all tasks for a given user. + * @param userId + */ + abstract tasks$(userId: UserId): Observable; + + /** + * Observable of pending tasks for a given user. + * @param userId + */ + abstract pendingTasks$(userId: UserId): Observable; + + /** + * Retrieves tasks from the API for a given user and updates the local state. + * @param userId + */ + abstract refreshTasks(userId: UserId): Promise; + + /** + * Clears all the tasks from state for the given user. + * @param userId + */ + abstract clear(userId: UserId): Promise; + + /** + * Marks a task as complete in local state and updates the server. + * @param taskId - The ID of the task to mark as complete. + * @param userId - The user who is completing the task. + */ + abstract markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise; +} diff --git a/libs/vault/src/tasks/enums/index.ts b/libs/vault/src/tasks/enums/index.ts new file mode 100644 index 00000000000..c1a5e81d877 --- /dev/null +++ b/libs/vault/src/tasks/enums/index.ts @@ -0,0 +1,2 @@ +export * from "./security-task-status.enum"; +export * from "./security-task-type.enum"; diff --git a/libs/vault/src/tasks/enums/security-task-status.enum.ts b/libs/vault/src/tasks/enums/security-task-status.enum.ts new file mode 100644 index 00000000000..1c6e7decc20 --- /dev/null +++ b/libs/vault/src/tasks/enums/security-task-status.enum.ts @@ -0,0 +1,11 @@ +export enum SecurityTaskStatus { + /** + * Default status for newly created tasks that have not been completed. + */ + Pending = 0, + + /** + * Status when a task is considered complete and has no remaining actions + */ + Completed = 1, +} diff --git a/libs/vault/src/tasks/enums/security-task-type.enum.ts b/libs/vault/src/tasks/enums/security-task-type.enum.ts new file mode 100644 index 00000000000..264cd88696b --- /dev/null +++ b/libs/vault/src/tasks/enums/security-task-type.enum.ts @@ -0,0 +1,6 @@ +export enum SecurityTaskType { + /** + * Task to update a cipher's password that was found to be at-risk by an administrator + */ + UpdateAtRiskCredential = 0, +} diff --git a/libs/vault/src/tasks/index.ts b/libs/vault/src/tasks/index.ts new file mode 100644 index 00000000000..a0339c6d8f4 --- /dev/null +++ b/libs/vault/src/tasks/index.ts @@ -0,0 +1,5 @@ +export * from "./enums"; +export * from "./models"; + +export * from "./abstractions/task.service"; +export * from "./services/default-task.service"; diff --git a/libs/vault/src/tasks/models/index.ts b/libs/vault/src/tasks/models/index.ts new file mode 100644 index 00000000000..7e31c136629 --- /dev/null +++ b/libs/vault/src/tasks/models/index.ts @@ -0,0 +1 @@ +export * from "./security-task"; diff --git a/libs/vault/src/tasks/models/security-task.data.ts b/libs/vault/src/tasks/models/security-task.data.ts new file mode 100644 index 00000000000..e2d9cc76c0a --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.data.ts @@ -0,0 +1,34 @@ +import { Jsonify } from "type-fest"; + +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +import { SecurityTaskResponse } from "./security-task.response"; + +export class SecurityTaskData { + id: SecurityTaskId; + organizationId: OrganizationId; + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(response: SecurityTaskResponse) { + this.id = response.id; + this.organizationId = response.organizationId; + this.cipherId = response.cipherId; + this.type = response.type; + this.status = response.status; + this.creationDate = response.creationDate; + this.revisionDate = response.revisionDate; + } + + static fromJSON(obj: Jsonify) { + return Object.assign(new SecurityTaskData({} as SecurityTaskResponse), obj, { + creationDate: new Date(obj.creationDate), + revisionDate: new Date(obj.revisionDate), + }); + } +} diff --git a/libs/vault/src/tasks/models/security-task.response.ts b/libs/vault/src/tasks/models/security-task.response.ts new file mode 100644 index 00000000000..2a335eb5d2f --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.response.ts @@ -0,0 +1,28 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +export class SecurityTaskResponse extends BaseResponse { + id: SecurityTaskId; + organizationId: OrganizationId; + /** + * Optional cipherId for tasks that are related to a cipher. + */ + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.organizationId = this.getResponseProperty("OrganizationId"); + this.cipherId = this.getResponseProperty("CipherId") || undefined; + this.type = this.getResponseProperty("Type"); + this.status = this.getResponseProperty("Status"); + this.creationDate = this.getResponseProperty("CreationDate"); + this.revisionDate = this.getResponseProperty("RevisionDate"); + } +} diff --git a/libs/vault/src/tasks/models/security-task.ts b/libs/vault/src/tasks/models/security-task.ts new file mode 100644 index 00000000000..42adb951945 --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.ts @@ -0,0 +1,28 @@ +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +import { SecurityTaskData } from "./security-task.data"; + +export class SecurityTask { + id: SecurityTaskId; + organizationId: OrganizationId; + /** + * Optional cipherId for tasks that are related to a cipher. + */ + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(obj: SecurityTaskData) { + this.id = obj.id; + this.organizationId = obj.organizationId; + this.cipherId = obj.cipherId; + this.type = obj.type; + this.status = obj.status; + this.creationDate = obj.creationDate; + this.revisionDate = obj.revisionDate; + } +} diff --git a/libs/vault/src/tasks/services/default-task.service.spec.ts b/libs/vault/src/tasks/services/default-task.service.spec.ts new file mode 100644 index 00000000000..26b1a79ca2e --- /dev/null +++ b/libs/vault/src/tasks/services/default-task.service.spec.ts @@ -0,0 +1,261 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { DefaultTaskService, SecurityTaskStatus } from "@bitwarden/vault"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../common/spec"; +import { SecurityTaskData } from "../models/security-task.data"; +import { SecurityTaskResponse } from "../models/security-task.response"; +import { SECURITY_TASKS } from "../state/security-task.state"; + +describe("Default task service", () => { + let fakeStateProvider: FakeStateProvider; + + const mockApiSend = jest.fn(); + const mockGetAllOrgs$ = jest.fn(); + + let testBed: TestBed; + + beforeEach(async () => { + mockApiSend.mockClear(); + mockGetAllOrgs$.mockClear(); + + fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); + testBed = TestBed.configureTestingModule({ + imports: [], + providers: [ + DefaultTaskService, + { + provide: StateProvider, + useValue: fakeStateProvider, + }, + { + provide: ApiService, + useValue: { + send: mockApiSend, + }, + }, + { + provide: OrganizationService, + useValue: { + organizations$: mockGetAllOrgs$, + }, + }, + ], + }); + }); + + describe("tasksEnabled$", () => { + it("should emit true if any organization uses risk insights", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + { + useRiskInsights: true, + }, + ] as Organization[]), + ); + + const { tasksEnabled$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasksEnabled$("user-id" as UserId)); + + expect(result).toBe(true); + }); + + it("should emit false if no organization uses risk insights", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + { + useRiskInsights: false, + }, + ] as Organization[]), + ); + + const { tasksEnabled$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasksEnabled$("user-id" as UserId)); + + expect(result).toBe(false); + }); + }); + + describe("tasks$", () => { + it("should fetch tasks from the API when the state is null", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null); + + const { tasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + }); + + it("should use the tasks from state when not null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const { tasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + + it("should share the same observable for the same user", async () => { + const { tasks$ } = testBed.inject(DefaultTaskService); + + const first = tasks$("user-id" as UserId); + const second = tasks$("user-id" as UserId); + + expect(first).toBe(second); + }); + }); + + describe("pendingTasks$", () => { + it("should filter tasks to only pending tasks", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "completed-task-id" as SecurityTaskId, + status: SecurityTaskStatus.Completed, + }, + { + id: "pending-task-id" as SecurityTaskId, + status: SecurityTaskStatus.Pending, + }, + ] as SecurityTaskData[]); + + const { pendingTasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(pendingTasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(result[0].id).toBe("pending-task-id" as SecurityTaskId); + }); + }); + + describe("refreshTasks()", () => { + it("should fetch tasks from the API", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + const service = testBed.inject(DefaultTaskService); + + await service.refreshTasks("user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + }); + + it("should update the local state with refreshed tasks", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null); + + const service = testBed.inject(DefaultTaskService); + + await service.refreshTasks("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + }); + }); + + describe("clear()", () => { + it("should clear the local state for the user", async () => { + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const service = testBed.inject(DefaultTaskService); + + await service.clear("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([]); + }); + }); + + describe("markAsComplete()", () => { + it("should send an API request to mark the task as complete", async () => { + const service = testBed.inject(DefaultTaskService); + + await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith( + "PATCH", + "/tasks/task-id/complete", + null, + true, + false, + ); + }); + + it("should refresh all tasks for the user after marking the task as complete", async () => { + mockApiSend + .mockResolvedValueOnce(null) // Mark as complete + .mockResolvedValueOnce({ + // Refresh tasks + data: [ + { + id: "new-task-id", + }, + ] as SecurityTaskResponse[], + }); + + const mockState = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "old-task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const service = testBed.inject(DefaultTaskService); + + await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + expect(mockState.nextMock).toHaveBeenCalledWith([ + { + id: "new-task-id", + } as SecurityTaskData, + ]); + }); + }); +}); diff --git a/libs/vault/src/tasks/services/default-task.service.ts b/libs/vault/src/tasks/services/default-task.service.ts new file mode 100644 index 00000000000..f5c1d95af08 --- /dev/null +++ b/libs/vault/src/tasks/services/default-task.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from "@angular/core"; +import { map, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/vault"; + +import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities"; +import { SecurityTaskData } from "../models/security-task.data"; +import { SecurityTaskResponse } from "../models/security-task.response"; +import { SECURITY_TASKS } from "../state/security-task.state"; + +@Injectable() +export class DefaultTaskService implements TaskService { + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + private organizationService: OrganizationService, + ) {} + + tasksEnabled$ = perUserCache$((userId) => { + return this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))); + }); + + tasks$ = perUserCache$((userId) => { + return this.taskState(userId).state$.pipe( + switchMap(async (tasks) => { + if (tasks == null) { + await this.fetchTasksFromApi(userId); + } + return tasks; + }), + filterOutNullish(), + map((tasks) => tasks.map((t) => new SecurityTask(t))), + ); + }); + + pendingTasks$ = perUserCache$((userId) => { + return this.tasks$(userId).pipe( + map((tasks) => tasks.filter((t) => t.status === SecurityTaskStatus.Pending)), + ); + }); + + async refreshTasks(userId: UserId): Promise { + await this.fetchTasksFromApi(userId); + } + + async clear(userId: UserId): Promise { + await this.updateTaskState(userId, []); + } + + async markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise { + await this.apiService.send("PATCH", `/tasks/${taskId}/complete`, null, true, false); + await this.refreshTasks(userId); + } + + /** + * Fetches the tasks from the API and updates the local state + * @param userId + * @private + */ + private async fetchTasksFromApi(userId: UserId): Promise { + const r = await this.apiService.send("GET", "/tasks", null, true, true); + const response = new ListResponse(r, SecurityTaskResponse); + + const taskData = response.data.map((t) => new SecurityTaskData(t)); + await this.updateTaskState(userId, taskData); + } + + /** + * Returns the local state for the tasks + * @param userId + * @private + */ + private taskState(userId: UserId) { + return this.stateProvider.getUser(userId, SECURITY_TASKS); + } + + /** + * Updates the local state with the provided tasks and returns the updated state + * @param userId + * @param tasks + * @private + */ + private updateTaskState(userId: UserId, tasks: SecurityTaskData[]): Promise { + return this.taskState(userId).update(() => tasks); + } +} diff --git a/libs/vault/src/tasks/state/security-task.state.ts b/libs/vault/src/tasks/state/security-task.state.ts new file mode 100644 index 00000000000..b86a891f008 --- /dev/null +++ b/libs/vault/src/tasks/state/security-task.state.ts @@ -0,0 +1,14 @@ +import { Jsonify } from "type-fest"; + +import { SECURITY_TASKS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; + +import { SecurityTaskData } from "../models/security-task.data"; + +export const SECURITY_TASKS = UserKeyDefinition.array( + SECURITY_TASKS_DISK, + "securityTasks", + { + deserializer: (task: Jsonify) => SecurityTaskData.fromJSON(task), + clearOn: ["logout", "lock"], + }, +); diff --git a/libs/vault/src/utils/observable-utilities.ts b/libs/vault/src/utils/observable-utilities.ts new file mode 100644 index 00000000000..bb559c600d3 --- /dev/null +++ b/libs/vault/src/utils/observable-utilities.ts @@ -0,0 +1,37 @@ +import { filter, Observable, OperatorFunction, shareReplay } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +/** + * Builds an observable once per userId and caches it for future requests. + * The built observables are shared among subscribers with a replay buffer size of 1. + * @param create - A function that creates an observable for a given userId. + */ +export function perUserCache$( + create: (userId: UserId) => Observable, +): (userId: UserId) => Observable { + const cache = new Map>(); + return (userId: UserId) => { + let observable = cache.get(userId); + if (!observable) { + observable = create(userId).pipe(shareReplay({ bufferSize: 1, refCount: false })); + cache.set(userId, observable); + } + return observable; + }; +} + +/** + * Strongly typed observable operator that filters out null/undefined values and adjusts the return type to + * be non-nullable. + * + * @example + * ```ts + * const source$ = of(1, null, 2, undefined, 3); + * source$.pipe(filterOutNullish()).subscribe(console.log); + * // Output: 1, 2, 3 + * ``` + */ +export function filterOutNullish(): OperatorFunction { + return filter((v): v is T => v != null); +} diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index 6004a56fb55..e1515183f22 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -1,5 +1,24 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "resolveJsonModule": true, + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-components": ["../tools/generator/components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], + "@bitwarden/vault": ["../vault/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/package-lock.json b/package-lock.json index 7994c8b0c2b..67123e09249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,11 +38,11 @@ "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", - "bufferutil": "4.0.8", + "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.39.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", @@ -70,7 +70,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.69", + "tldts": "6.1.74", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" @@ -83,7 +83,6 @@ "@angular-eslint/template-parser": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", - "@angular/elements": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", @@ -99,6 +98,7 @@ "@storybook/angular": "8.4.7", "@storybook/manager-api": "8.4.7", "@storybook/theming": "8.4.7", + "@storybook/web-components-webpack5": "8.4.7", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.280", "@types/firefox-webext-browser": "120.0.4", @@ -113,7 +113,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.2", + "@types/node": "22.10.7", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -121,8 +121,8 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", @@ -130,43 +130,43 @@ "base64-loader": "1.0.0", "browserslist": "4.23.2", "chromatic": "11.20.2", - "concurrently": "9.1.0", + "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.2.1", + "electron": "34.0.0", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.3.9", - "eslint": "8.57.0", - "eslint-config-prettier": "9.1.0", - "eslint-import-resolver-typescript": "3.6.1", - "eslint-plugin-import": "2.29.1", + "eslint": "8.57.1", + "eslint-config-prettier": "10.0.1", + "eslint-import-resolver-typescript": "3.7.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", - "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.17.4", + "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-tailwindcss": "3.17.5", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", - "husky": "9.1.4", - "jest-extended": "4.0.2", + "husky": "9.1.7", + "jest-diff": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.2.8", + "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", "postcss": "8.4.49", "postcss-loader": "8.1.1", "prettier": "3.4.2", - "prettier-plugin-tailwindcss": "0.6.9", + "prettier-plugin-tailwindcss": "0.6.10", "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.83.4", "sass-loader": "16.0.4", "storybook": "8.4.7", "style-loader": "4.0.0", @@ -179,7 +179,7 @@ "typescript-strict-plugin": "^2.4.4", "url": "0.11.4", "util": "0.12.5", - "wait-on": "8.0.1", + "wait-on": "8.0.2", "webpack": "5.97.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", @@ -192,11 +192,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.1.0" + "version": "2025.1.4" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.1.0", + "version": "2025.1.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -206,7 +206,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jsdom": "25.0.1", @@ -223,7 +223,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.69", + "tldts": "6.1.74", "zxcvbn": "4.4.2" }, "bin": { @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.1.0", + "version": "2025.1.8", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -246,7 +246,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.1.0" + "version": "2025.1.2" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -950,18 +950,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { "version": "5.94.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", @@ -1324,18 +1312,6 @@ "node": ">= 4" } }, - "node_modules/@angular-eslint/schematics/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular-eslint/template-parser": { "version": "18.4.3", "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.3.tgz", @@ -1853,18 +1829,6 @@ "node": ">=14.0.0" } }, - "node_modules/@angular/build/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/build/node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -2060,18 +2024,6 @@ "node": ">=18.0.0" } }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/cli/node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -2287,22 +2239,6 @@ "zone.js": "~0.14.10" } }, - "node_modules/@angular/elements": { - "version": "18.2.13", - "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-18.2.13.tgz", - "integrity": "sha512-yahRkXWgFolpWMeVsTIlWYwoq4Bsz6wrfS4b+TKHIZbTCyRUlJ5zBFecpYMwgmVuQDJZp+WkUWZV2Qg7kLJR5w==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.2.13", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@angular/forms": { "version": "18.2.13", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.13.tgz", @@ -2730,9 +2666,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5058,19 +4994,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@compodoc/compodoc/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@compodoc/compodoc/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -5921,9 +5844,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { @@ -6029,14 +5952,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -7515,6 +7438,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@npmcli/agent": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", @@ -8006,6 +7939,13 @@ "darwin" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@schematics/angular": { "version": "18.2.12", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.12.tgz", @@ -8873,19 +8813,6 @@ "storybook": "^8.4.7" } }, - "node_modules/@storybook/core/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@storybook/csf": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", @@ -9034,6 +8961,56 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, + "node_modules/@storybook/web-components": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.4.7.tgz", + "integrity": "sha512-zR/bUWGkS5uxvqfXnW082ScrC4y5UrTdE1VKasezLGi5bTLub2hz8JP87PJgtWrq+mdrdmkLGzv5O4iJ/tlMAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/components": "8.4.7", + "@storybook/global": "^5.0.0", + "@storybook/manager-api": "8.4.7", + "@storybook/preview-api": "8.4.7", + "@storybook/theming": "8.4.7", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "lit": "^2.0.0 || ^3.0.0", + "storybook": "^8.4.7" + } + }, + "node_modules/@storybook/web-components-webpack5": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/web-components-webpack5/-/web-components-webpack5-8.4.7.tgz", + "integrity": "sha512-RgLFQB7F4FOX5nOK3byaCo5Gs8nKMq1uNswOXdHSgZKfJfaZxmyMMGmnVUmOOLECsxyREokHwRDKma8SgFrRRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/builder-webpack5": "8.4.7", + "@storybook/web-components": "8.4.7", + "@types/node": "^22.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "lit": "^2.0.0 || ^3.0.0", + "storybook": "^8.4.7" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -9772,9 +9749,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, "license": "MIT", "dependencies": { @@ -10099,88 +10076,33 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", - "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", + "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/type-utils": "8.20.0", + "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", - "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", - "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/experimental-utils": { @@ -10333,220 +10255,168 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", - "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", + "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", - "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", + "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1" + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/types": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", - "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", + "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", - "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", - "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/utils": "8.20.0", "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", - "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/typescript-estree": "7.11.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", - "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "node_modules/@typescript-eslint/types": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", + "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0" - }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", - "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", + "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", - "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", - "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/types": "7.11.0", - "eslint-visitor-keys": "^3.4.3" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", - "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", + "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.20.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -12009,9 +11879,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dev": true, "license": "MIT", "dependencies": { @@ -12719,9 +12589,9 @@ "license": "MIT" }, "node_modules/bufferutil": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", - "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -13825,9 +13695,9 @@ } }, "node_modules/concurrently": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", - "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14601,9 +14471,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -15335,9 +15205,9 @@ } }, "node_modules/electron": { - "version": "33.2.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", - "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.0.0.tgz", + "integrity": "sha512-fpaPb0lifoUJ6UJa4Lk8/0B2Ku/xDZWdc1Gkj67jbygTCrvSon0qquju6Ltx1Kz23GRqqlIHXiy9EvrjpY7/Wg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15562,23 +15432,10 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", - "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", + "version": "20.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz", + "integrity": "sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==", "dev": true, "license": "MIT", "dependencies": { @@ -16031,9 +15888,9 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", @@ -16041,8 +15898,8 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -16088,13 +15945,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -16139,19 +15996,20 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "dev": true, "license": "ISC", "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -16161,7 +16019,16 @@ }, "peerDependencies": { "eslint": "*", - "eslint-plugin-import": "*" + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, "node_modules/eslint-module-utils": { @@ -16193,35 +16060,37 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { @@ -16322,167 +16191,27 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.8.0.tgz", - "integrity": "sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.2.tgz", + "integrity": "sha512-0Z4DUklJrC+GHjCRXa7PYfPzWC15DaVnwaOYenpgXiCEijXPZkLKCms+rHhtoRcWccP7Z8DpOOaP1gc3P9oOwg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf": "^0.0.1", - "@typescript-eslint/utils": "^5.62.0", - "requireindex": "^1.2.0", + "@storybook/csf": "^0.1.11", + "@typescript-eslint/utils": "^8.8.1", "ts-dedent": "^2.2.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "eslint": ">=6" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@storybook/csf": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.1.tgz", - "integrity": "sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "eslint": ">=8" } }, "node_modules/eslint-plugin-tailwindcss": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.4.tgz", - "integrity": "sha512-gJAEHmCq2XFfUP/+vwEfEJ9igrPeZFg+skeMtsxquSQdxba9XRk5bn0Bp9jxG1VV9/wwPKi1g3ZjItu6MIjhNg==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.5.tgz", + "integrity": "sha512-8Mi7p7dm+mO1dHgRHHFdPu4RDTBk69Cn4P0B40vRQR+MrguUpwmKwhZy1kqYe3Km8/4nb+cyrCF+5SodOEmaow==", "dev": true, "license": "MIT", "dependencies": { @@ -17755,9 +17484,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -18912,9 +18641,9 @@ } }, "node_modules/husky": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz", - "integrity": "sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -19444,6 +19173,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -20720,28 +20459,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-extended": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", - "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-diff": "^29.0.0", - "jest-get-type": "^29.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "jest": ">=27.2.5" - }, - "peerDependenciesMeta": { - "jest": { - "optional": true - } - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -22300,22 +22017,22 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.8.tgz", - "integrity": "sha512-PUWFf2zQzsd9EFU+kM1d7UP+AZDbKFKuj+9JNVTBkhUFhbg4MAt6WfyMMwBfM4lYqd4D2Jwac5iuTu9rVj4zCQ==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.1.tgz", + "integrity": "sha512-P8yJuVRyLrm5KxCtFx+gjI5Bil+wO7wnTl7C3bXhvtTaAFGirzeB24++D0wGoUwxrUKecNiehemgCob9YL39NA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "~5.3.0", + "chalk": "~5.4.1", "commander": "~12.1.0", - "debug": "~4.3.6", + "debug": "~4.4.0", "execa": "~8.0.1", - "lilconfig": "~3.1.2", - "listr2": "~8.2.4", - "micromatch": "~4.0.7", + "lilconfig": "~3.1.3", + "listr2": "~8.2.5", + "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", - "yaml": "~2.5.0" + "yaml": "~2.6.1" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -22328,9 +22045,9 @@ } }, "node_modules/lint-staged/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, "license": "MIT", "engines": { @@ -27212,9 +26929,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz", - "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==", + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.10.tgz", + "integrity": "sha512-ndj2WLDaMzACnr1gAYZiZZLs5ZdOeBYgOsbBmHj3nvW/6q8h8PymsXiEnKvj/9qgCCAoHyvLOisoQdIcsDvIgw==", "dev": true, "license": "MIT", "engines": { @@ -27225,7 +26942,7 @@ "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig-melody": "*", + "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", @@ -27252,7 +26969,7 @@ "@trivago/prettier-plugin-sort-imports": { "optional": true }, - "@zackad/prettier-plugin-twig-melody": { + "@zackad/prettier-plugin-twig": { "optional": true }, "prettier-plugin-astro": { @@ -28777,9 +28494,9 @@ } }, "node_modules/sass": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", "dev": true, "license": "MIT", "dependencies": { @@ -28839,9 +28556,9 @@ } }, "node_modules/sass/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -28862,13 +28579,13 @@ "license": "MIT" }, "node_modules/sass/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -28964,13 +28681,10 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -28986,24 +28700,6 @@ "license": "MIT", "optional": true }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/send": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", @@ -29725,6 +29421,13 @@ "dev": true, "license": "ISC" }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -30719,21 +30422,21 @@ } }, "node_modules/tldts": { - "version": "6.1.69", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.69.tgz", - "integrity": "sha512-Oh/CqRQ1NXNY7cy9NkTPUauOWiTro0jEYZTioGbOmcQh6EC45oribyIMJp0OJO3677r13tO6SKdWoGZUx2BDFw==", + "version": "6.1.74", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.74.tgz", + "integrity": "sha512-O5vTZ1UmmEmrLl/59U9igitnSMlprALLaLgbv//dEvjobPT9vyURhHXKMCDLEhn3qxZFIkb9PwAfNYV0Ol7RPQ==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.69" + "tldts-core": "^6.1.74" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.69", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.69.tgz", - "integrity": "sha512-nygxy9n2PBUFQUtAXAc122gGo+04/j5qr5TGQFZTHafTKYvmARVXt2cA5rgero2/dnXUfkdPtiJoKmrd3T+wdA==", + "version": "6.1.74", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.74.tgz", + "integrity": "sha512-gTwtY6L2GfuxiL4CWpLknv9JDYYqBvKCk/BT5uAaAvCA0s6pzX7lr2IrkQZSUlnSjRHIjTl8ZwKCVXJ7XNRWYw==", "license": "MIT" }, "node_modules/tmp": { @@ -30870,16 +30573,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", - "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-dedent": { @@ -32212,13 +31915,13 @@ } }, "node_modules/wait-on": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz", - "integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.2.tgz", + "integrity": "sha512-qHlU6AawrgAIHlueGQHQ+ETcPLAauXbnoTKl3RKq20W0T8x0DKVAo5xWIYjHSyvHxQlcYbFdR0jp4T9bDVITFA==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.7.7", + "axios": "^1.7.9", "joi": "^17.13.3", "lodash": "^4.17.21", "minimist": "^1.2.8", @@ -33112,9 +32815,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 0a78d370d26..895f1d844c9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:watch:all": "jest --watchAll", "test:types": "node ./scripts/test-types.js", "test:locales": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/test-locales.js", + "lint:dep-ownership": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/dep-ownership.js", "docs:json": "compodoc -p ./tsconfig.json -e json -d . --disableRoutesGraph", "storybook": "ng run components:storybook", "build-storybook": "ng run components:build-storybook", @@ -42,7 +43,6 @@ "@angular-eslint/template-parser": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", - "@angular/elements": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", @@ -58,6 +58,7 @@ "@storybook/angular": "8.4.7", "@storybook/manager-api": "8.4.7", "@storybook/theming": "8.4.7", + "@storybook/web-components-webpack5": "8.4.7", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.280", "@types/firefox-webext-browser": "120.0.4", @@ -72,7 +73,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.2", + "@types/node": "22.10.7", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -80,8 +81,8 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", @@ -89,43 +90,43 @@ "base64-loader": "1.0.0", "browserslist": "4.23.2", "chromatic": "11.20.2", - "concurrently": "9.1.0", + "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.2.1", + "electron": "34.0.0", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.3.9", - "eslint": "8.57.0", - "eslint-config-prettier": "9.1.0", - "eslint-import-resolver-typescript": "3.6.1", - "eslint-plugin-import": "2.29.1", + "eslint": "8.57.1", + "eslint-config-prettier": "10.0.1", + "eslint-import-resolver-typescript": "3.7.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", - "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.17.4", + "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-tailwindcss": "3.17.5", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", - "husky": "9.1.4", - "jest-extended": "4.0.2", + "husky": "9.1.7", + "jest-diff": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.2.8", + "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", "postcss": "8.4.49", "postcss-loader": "8.1.1", "prettier": "3.4.2", - "prettier-plugin-tailwindcss": "0.6.9", + "prettier-plugin-tailwindcss": "0.6.10", "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.83.4", "sass-loader": "16.0.4", "storybook": "8.4.7", "style-loader": "4.0.0", @@ -138,7 +139,7 @@ "typescript-strict-plugin": "^2.4.4", "url": "0.11.4", "util": "0.12.5", - "wait-on": "8.0.1", + "wait-on": "8.0.2", "webpack": "5.97.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", @@ -168,11 +169,11 @@ "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", - "bufferutil": "4.0.8", + "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.39.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", @@ -200,7 +201,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.69", + "tldts": "6.1.74", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" diff --git a/scripts/dep-ownership.ts b/scripts/dep-ownership.ts new file mode 100644 index 00000000000..e574a3e9e96 --- /dev/null +++ b/scripts/dep-ownership.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ + +/// Ensure that all dependencies in package.json have an owner in the renovate.json file. + +import fs from "fs"; +import path from "path"; + +const renovateConfig = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json"), "utf8"), +); + +const packagesWithOwners = renovateConfig.packageRules + .flatMap((rule: any) => rule.matchPackageNames) + .filter((packageName: string) => packageName != null); + +const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8"), +); +const dependencies = Object.keys(packageJson.dependencies).concat( + Object.keys(packageJson.devDependencies), +); + +const missingOwners = dependencies.filter((dep) => !packagesWithOwners.includes(dep)); + +if (missingOwners.length > 0) { + console.error("Missing owners for the following dependencies:"); + console.error(missingOwners.join("\n")); + process.exit(1); +} + +console.log("All dependencies have owners."); diff --git a/scripts/test-types.js b/scripts/test-types.js index 2155406cdf0..9534558af30 100644 --- a/scripts/test-types.js +++ b/scripts/test-types.js @@ -16,10 +16,15 @@ function getFiles(dir) { return results; } -const files = getFiles(path.join(__dirname, "..", "libs")).filter((file) => { - const name = path.basename(file); - return name === "tsconfig.spec.json"; -}); +const files = getFiles(path.join(__dirname, "..", "libs")) + .filter((file) => { + const name = path.basename(file); + return name === "tsconfig.spec.json"; + }) + .filter((path) => { + // Exclude shared since it's not actually a library + return !path.includes("libs/shared/"); + }); concurrently([ { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 80f70e47b09..980d7832ace 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -14,11 +14,13 @@ "declarationDir": "dist/types", "outDir": "dist", "baseUrl": ".", + "allowJs": true, "paths": { "@bitwarden/admin-console": ["./libs/admin-console/src"], "@bitwarden/angular/*": ["./libs/angular/src/*"], "@bitwarden/auth": ["./libs/auth/src"], "@bitwarden/billing": ["./libs/billing/src"], + "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], @@ -26,16 +28,18 @@ "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], + "@bitwarden/key-management": ["./libs/key-management/src"], + "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], + "@bitwarden/node/*": ["./libs/node/src/*"], + "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": [".libs/tools/send/send-ui/src"], "@bitwarden/tools-card": [".libs/tools/card/src"], - "@bitwarden/platform": ["./libs/platform/src"], - "@bitwarden/node/*": ["./libs/node/src/*"], - "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/ui-common": ["./libs/ui/common/src"], + "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], + "@bitwarden/vault": ["./libs/vault/src"] }, "plugins": [ { diff --git a/tsconfig.json b/tsconfig.json index 91b4ee7dd6b..efa2ff70e1b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,9 +18,10 @@ "paths": { "@bitwarden/admin-console/common": ["./libs/admin-console/src/common"], "@bitwarden/angular/*": ["./libs/angular/src/*"], - "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/auth/angular": ["./libs/auth/src/angular"], + "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/billing": ["./libs/billing/src"], + "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], @@ -28,19 +29,19 @@ "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/key-management": ["./libs/key-management/src"], "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], + "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["./libs/tools/card/src"], - "@bitwarden/node/*": ["./libs/node/src/*"], - "@bitwarden/web-vault/*": ["./apps/web/src/*"], + "@bitwarden/ui-common": ["./libs/ui/common/src"], + "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/web-vault/*": ["./apps/web/src/*"] }, "plugins": [ {