diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c7c43c4dac..2f402b15dd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,8 +83,6 @@ libs/common/src/platform @bitwarden/team-platform-dev libs/common/spec @bitwarden/team-platform-dev libs/common/src/state-migrations @bitwarden/team-platform-dev libs/platform @bitwarden/team-platform-dev -# Node-specifc platform files -libs/node @bitwarden/team-key-management-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files @@ -146,6 +144,8 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev +# Node-cryptofunction service +libs/node @bitwarden/team-key-management-dev apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev diff --git a/.github/DISCUSSION_TEMPLATE/password-manager.yml b/.github/DISCUSSION_TEMPLATE/password-manager.yml index bc3938c1962..1d464ca9504 100644 --- a/.github/DISCUSSION_TEMPLATE/password-manager.yml +++ b/.github/DISCUSSION_TEMPLATE/password-manager.yml @@ -3,7 +3,7 @@ body: - type: markdown attributes: value: | - If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/ + If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. - type: dropdown attributes: label: Select Topic Area diff --git a/.github/DISCUSSION_TEMPLATE/secrets-manager.yml b/.github/DISCUSSION_TEMPLATE/secrets-manager.yml index bc3938c1962..1d464ca9504 100644 --- a/.github/DISCUSSION_TEMPLATE/secrets-manager.yml +++ b/.github/DISCUSSION_TEMPLATE/secrets-manager.yml @@ -3,7 +3,7 @@ body: - type: markdown attributes: value: | - If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/ + If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. - type: dropdown attributes: label: Select Topic Area diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 973ba700349..c4202ed2a68 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -250,6 +250,7 @@ "napi-derive", "node-forge", "node-ipc", + "nx", "oo7", "oslog", "pin-project", diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1ccc3d4bc8a..c75691f2d0a 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -87,6 +87,7 @@ jobs: os: [ { base: "linux", distro: "ubuntu-22.04", target_suffix: "" }, + { base: "linux", distro: "ubuntu-22.04-arm", target_suffix: "-arm64" }, { base: "mac", distro: "macos-13", target_suffix: "" }, { base: "mac", distro: "macos-14", target_suffix: "-arm64" } ] @@ -130,7 +131,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -370,7 +371,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 4c35eb4c42f..365d29f17f7 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -201,7 +201,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -237,7 +237,7 @@ jobs: TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - node build.js cross-platform + node build.js --target=x86_64-unknown-linux-musl --release - name: Build application run: npm run dist:lin @@ -298,6 +298,103 @@ jobs: if-no-files-found: error + linux-arm64: + name: Linux ARM64 Build + # Note, before updating the ubuntu version of the workflow, ensure the snap base image + # is equal or greater than the new version. Otherwise there might be GLIBC version issues. + # The snap base for desktop is defined in `apps/desktop/electron-builder.json` + # We intentionally keep this runner on the oldest supported OS in GitHub Actions + # for maximum compatibility across GLIBC versions + runs-on: ubuntu-22.04-arm + needs: setup + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + _NODE_VERSION: ${{ needs.setup.outputs.node_version }} + NODE_OPTIONS: --max_old_space_size=4096 + defaults: + run: + working-directory: apps/desktop + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + node-version: ${{ env._NODE_VERSION }} + + - name: Set up environment + run: | + sudo apt-get update + sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder + + - name: Print environment + run: | + node --version + npm --version + snap --version + snapcraft --version || echo 'snapcraft unavailable' + + - name: Install Node dependencies + run: npm ci + working-directory: ./ + + - name: Download SDK Artifacts + if: ${{ inputs.sdk_branch != '' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: sdk-internal + repo: bitwarden/sdk-internal + path: ../sdk-internal + if_no_artifact_found: fail + + - name: Override SDK + if: ${{ inputs.sdk_branch != '' }} + working-directory: ./ + run: | + ls -l ../ + npm link ../sdk-internal + + - name: Cache Native Module + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: cache + with: + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* + ${{ env.RUNNER_TEMP }}/.cargo/registry + ${{ env.RUNNER_TEMP }}/.cargo/git + key: rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} + + - name: Build Native Module + if: steps.cache.outputs.cache-hit != 'true' + working-directory: apps/desktop/desktop_native + env: + PKG_CONFIG_ALLOW_CROSS: true + PKG_CONFIG_ALL_STATIC: true + TARGET: musl + run: | + rustup target add aarch64-unknown-linux-musl + node build.js --target=aarch64-unknown-linux-musl --release + + - name: Build application + run: npm run dist:lin:arm64 + + - name: Upload tar.gz artifact + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.tar.gz + path: apps/desktop/dist/bitwarden_desktop_arm64.tar.gz + if-no-files-found: error + windows: name: Windows Build runs-on: windows-2022 @@ -369,7 +466,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -705,7 +802,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -895,7 +992,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -1144,7 +1241,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -1425,7 +1522,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 12748a47748..3da524702fe 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -45,7 +45,7 @@ on: env: _AZ_REGISTRY: bitwardenprod.azurecr.io - + _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} jobs: setup: @@ -190,12 +190,18 @@ jobs: - name: Generate container image tag id: tag run: | - if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then + IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF_NAME}" | sed "s#/#-#g") fi + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only + IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags + fi + if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 9b890491282..1cde8dd636a 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -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: diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 09f5ddc6318..69b29086d36 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -141,3 +141,36 @@ jobs: - name: Log out of Docker run: docker logout + + self-host-unified-build: + name: Trigger self-host unified build + runs-on: ubuntu-22.04 + needs: + - setup + steps: + - name: Log in to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Trigger self-host build + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'self-host', + workflow_id: 'build-unified.yml', + ref: 'main', + inputs: { + use_latest_core_version: true + } + }); diff --git a/.gitignore b/.gitignore index d0d8edd596c..e865fa6a8fb 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ storybook-static # Local app configuration apps/**/config/local.json + +# Nx +.nx diff --git a/apps/browser/scripts/package-safari.ps1 b/apps/browser/scripts/package-safari.ps1 index ce208478098..1df40c68b37 100755 --- a/apps/browser/scripts/package-safari.ps1 +++ b/apps/browser/scripts/package-safari.ps1 @@ -52,7 +52,7 @@ foreach ($subBuildPath in $subBuildPaths) { "--verbose", "--force", "--sign", - "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C", + "588E3F1724AE018EBA762E42279DAE85B313E3ED", "--entitlements", $entitlementsPath ) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 3ca64543a4f..1bd6a77b746 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." @@ -3986,7 +4011,7 @@ "message": "رمز غير صحيح" }, "incorrectPin": { - "message": "رمز PIN غير صحيح" + "message": "رمز المرور غير صحيح" }, "multifactorAuthenticationFailed": { "message": "Multifactor authentication failed" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 07a86b42565..41e6d9b5ab9 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -1098,6 +1098,31 @@ "message": "Giriş məlumatları güncəlləndi", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Saxlama xətası", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 7240371ca2d..f828400c575 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 88128e68be1..457b0693a17 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -1098,6 +1098,31 @@ "message": "Данните за вписване са обновени", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Чудесна работа! Направихте правилните стъпки за Вашата защита и тази на $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Благодарим Ви, че подобрихте защитата на $ORGANIZATION$. Имате още $TASK_COUNT$ пароли за обновяване.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Промяна на следващата парола", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Грешка при запазването", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 7a7a9cfa981..275bb18e029 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 0e73dc0b92e..687610c777e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 3555642ef90..d9a24c5e473 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 361d5314e5d..2ed34c8c71a 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -1098,6 +1098,31 @@ "message": "Přihlašovací údaje byly aktualizovány", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Skvělá práce! Podnikli jste kroky, abyste $ORGANIZATION$ udělali bezpečnější.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Děkujeme, že jste udělali $ORGANIZATION$ bezpečnější. Máte $TASK_COUNT$ hesel k aktualizaci.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Změnit další heslo", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Chyba při ukládání", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c0034df9d2b..eb07358540d 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index c6f051ce811..914c74411e4 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 274bc934b39..0edf5edfa3d 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1098,6 +1098,31 @@ "message": "Zugangsdaten aktualisiert", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Fehler beim Speichern", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index afd76596c85..1c15f70d40c 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 586e7e1f2cf..87b94650b51 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." @@ -4903,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 3b968f11587..520af1ca8ff 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index f32abc028ed..f75664f10db 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index e031a4487d6..1400cb78845 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 670dab1fcda..f9f7cb950fe 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index b5ffbf45ba2..ec1f5189946 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index d8a8f827db4..0e5e8943b7d 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 56e25aead7d..4048925ebaa 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1098,6 +1098,31 @@ "message": "Kirjautumistieto päivitettiin", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Virhe tallennettaessa", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 69d5895ba27..9c3865bc0da 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index fe2127148c8..1f05f699ea9 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -1098,6 +1098,31 @@ "message": "Identifiant mis à jour", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Erreur lors de l'enregistrement", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 83af8b6db75..b2b843d7830 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 605dd5454f0..fd38df53906 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -1098,6 +1098,31 @@ "message": "כניסה עודכנה", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "שגיאה בשמירה", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 9cc0f5251e6..813b69b9079 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index bef7c9b8f13..896593332a4 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -1098,6 +1098,31 @@ "message": "Prijava ažurirana", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Greška kod spremanja", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index d0114b30b7b..f58e542c77f 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -1098,6 +1098,31 @@ "message": "A bejelentkezés frissítésre került.", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Remek munka! Megtettük a lépéseket, hogy magunk és $ORGANIZATION$ biztonságosabbá váljon.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Köszönet $ORGANIZATION$ biztonságosabbá válásához. További $TASK_COUNT$ frissítendő jelszó van még.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Következő jelszó megváltoztatása", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Hiba történt a bejelentkezés mentésekor.", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 16b11c9e732..101fcf43795 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -1098,6 +1098,31 @@ "message": "Log masuk diperbarui", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Kesalahan saat menyimpan", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 1448776e430..5f9a6ca7dca 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -1098,6 +1098,31 @@ "message": "Accesso aggiornato", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Errore nel salvataggio", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 4aae076174e..34b15699437 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -1098,6 +1098,31 @@ "message": "ログイン情報が更新されました", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "保存エラー", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 132a9b4d4ed..f261f69904d 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 9e137fd6156..7d9583dc652 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index dfc7364710a..7d65686df5d 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 93bbbef7292..2245fdb0029 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index a42ee2ace78..517dacfb72b 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -1098,6 +1098,31 @@ "message": "Pieteikšanās vienums atjaunināts", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Lielisks darbs! Tu attiecīgi rīkojies, lai padarītu savi un $ORGANIZATION$ drošāku.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Paldies par $ORGANIZATION$ padarīšanu drošāku! Tev ir vēl $TASK_COUNT$ paroļu, ko atjaunināt.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Mainīt nākamo paroli", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Kļūda saglabāšanas laikā", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 5810d5392e6..49a2695ce6f 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 303a2cf96dd..6b2f7b8bc32 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 18d00d9c274..7fa41eed68b 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 80eba21ddb3..dd101e62e75 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -1098,6 +1098,31 @@ "message": "Login bijgewerkt", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Goed gedaan! Je hebt stappen genomen om jou en $ORGANIZATION$ veiliger te maken.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Bedankt voor het veiliger maken van $ORGANIZATION$. Je moet nog $TASK_COUNT$ wachtwoorden bijwerken.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Volgend wachtwoord veranderen", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Fout bij opslaan", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 5e74025800e..3d25e6f123c 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1098,6 +1098,31 @@ "message": "Dane logowania zostały zaktualizowane", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Świetna robota! Podjęto kroki mające na celu zwiększenie bezpieczeństwa Twojego oraz $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Dziękujemy za dbanie o bezpieczeństwo $ORGANIZATION$. Pozostało $TASK_COUNT$ haseł do zaktualizowania.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Zmień następne hasło", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Błąd podczas zapisywania", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index f22958d0498..9d1744f5473 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1098,6 +1098,31 @@ "message": "Sessão atualizada", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Erro ao salvar", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 172e748a3f1..daab7fb05d9 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -1098,6 +1098,31 @@ "message": "Credencial atualizada", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Excelente trabalho! Tomou as medidas necessárias para o tornar a si e à sua $ORGANIZATION$ mais seguros.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Obrigado por tornar a $ORGANIZATION$ mais segura. Tem mais $TASK_COUNT$ palavras-passe para atualizar.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Alterar a palavra-passe seguinte", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Erro ao guardar", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 0272add140a..bcdf10f37f1 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 639a3b740db..b0f03d34f69 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -1098,6 +1098,31 @@ "message": "Логин обновлен", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Отличная работа! Вы предприняли шаги, чтобы сделать себя и $ORGANIZATION$ более защищенными.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Спасибо, что сделали $ORGANIZATION$ более защищенной. У вас есть еще $TASK_COUNT$ паролей для обновления.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Изменить следующий пароль", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Ошибка при сохранении", "description": "Error message shown when the system fails to save login details." @@ -1772,7 +1797,7 @@ "message": "Полное имя" }, "identityName": { - "message": "Имя" + "message": "Название личности" }, "company": { "message": "Компания" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 70c1ff7126a..4efbb2dceeb 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 0b0a41a49b2..b427e902334 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1098,6 +1098,31 @@ "message": "Prihlasovacie údaje aktualizované", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Skvelá práca! Podnikli ste kroky, aby ste vy a $ORGANIZATION$ boli bezpečnejší.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Ďakujeme, že ste pre $ORGANIZATION$ zabezpečili väčšiu bezpečnosť. Máte ďalšie ($TASK_COUNT$) heslá na aktualizáciu.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Zmeniť ďalšie heslo", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Chyba pri ukladaní", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 0fc3b26acb8..61a70650d16 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index c402372895e..a36006894ba 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1098,6 +1098,31 @@ "message": "Пријава ажурирана", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Грешка при снимању", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 74b9bbccc80..54a34e15013 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 0ef98835f18..b502e29cfdd 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index f009a52a951..8a00287dfcc 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -1098,6 +1098,31 @@ "message": "Hesap güncellendi", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Kaydetme hatası", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 11e5b304611..bdd96ba9ea4 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -1098,6 +1098,31 @@ "message": "Запис оновлено", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Чудова робота! Ви щойно підвищили рівень безпеки для себе та $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Дякуємо за підвищення рівня безпеки $ORGANIZATION$. Залишилося оновити ще $TASK_COUNT$ паролів.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Змінити наступний пароль", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Помилка збереження", "description": "Error message shown when the system fails to save login details." @@ -2126,11 +2151,11 @@ "message": "Надійний пароль згенеровано! Обов'язково оновіть свій пароль на вебсайті." }, "useGeneratorHelpTextPartOne": { - "message": "Скористатися генератором", + "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": "для створення надійного, унікального пароля", + "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'" }, "vaultCustomization": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4693c93af9c..c6c6ed7ee6d 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index c8eaacc8c86..662284568cd 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1098,6 +1098,31 @@ "message": "登录已更新", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "干得好!您采取了使您和 $ORGANIZATION$ 更加安全的措施。", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "感谢您使 $ORGANIZATION$ 更加安全。您还有 $TASK_COUNT$ 个密码需要更新。", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "更改下一个密码", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "保存时出错", "description": "Error message shown when the system fails to save login details." @@ -2825,7 +2850,7 @@ "message": "选择文件夹..." }, "noFoldersFound": { - "message": "找不到文件夹", + "message": "未找到任何文件夹", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -3929,7 +3954,7 @@ "message": "此应用程序已存在一个通行密钥。" }, "noPasskeysFoundForThisApplication": { - "message": "没有找到此应用程序的通行密钥。" + "message": "未找到此应用程序的通行密钥。" }, "noMatchingPasskeyLogin": { "message": "您没有匹配此站点的登录。" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1ba3a2166dd..7d56ecd8e63 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 851f07576dd..6b3c91a109c 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -95,6 +95,7 @@ type NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; + bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index ebdd244e140..ffc416ab62a 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; @@ -12,6 +12,7 @@ import { UserNotificationSettingsService } from "@bitwarden/common/autofill/serv import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -19,6 +20,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; +import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; @@ -46,6 +48,8 @@ jest.mock("rxjs", () => { }); describe("NotificationBackground", () => { + const messagingService = mock(); + const taskService = mock(); let notificationBackground: NotificationBackground; const autofillService = mock(); const cipherService = mock(); @@ -88,6 +92,8 @@ describe("NotificationBackground", () => { policyService, themeStateService, userNotificationSettingsService, + taskService, + messagingService, ); }); @@ -201,8 +207,8 @@ describe("NotificationBackground", () => { await flushPromises(); expect(notificationBackground["handleSaveCipherMessage"]).toHaveBeenCalledWith( - message.data.commandToRetry.message, - message.data.commandToRetry.sender, + message.data?.commandToRetry?.message, + message.data?.commandToRetry?.sender, ); }); }); @@ -498,7 +504,7 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( null, "example.com", - message.data.newPassword, + message.data?.newPassword, sender.tab, true, ); @@ -570,7 +576,7 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( "cipher-id", "example.com", - message.data.newPassword, + message.data?.newPassword, sender.tab, ); }); @@ -618,7 +624,7 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( "cipher-id", "example.com", - message.data.newPassword, + message.data?.newPassword, sender.tab, ); }); @@ -844,6 +850,86 @@ describe("NotificationBackground", () => { ); }); + it("completes password update notification with a security task notice if any are present for the cipher, and dismisses tasks for the updated cipher", async () => { + const mockCipherId = "testId"; + const mockOrgId = "testOrgId"; + const mockSecurityTask = { + id: "testTaskId", + organizationId: mockOrgId, + cipherId: mockCipherId, + type: 0, + status: 0, + creationDate: new Date(), + revisionDate: new Date(), + } as SecurityTask; + const mockSecurityTask2 = { + ...mockSecurityTask, + id: "testTaskId2", + cipherId: "testId2", + } as SecurityTask; + taskService.tasksEnabled$.mockImplementation(() => of(true)); + taskService.pendingTasks$.mockImplementation(() => + of([mockSecurityTask, mockSecurityTask2]), + ); + jest + .spyOn(notificationBackground as any, "getNotificationFlag") + .mockResolvedValueOnce(true); + jest.spyOn(notificationBackground as any, "getOrgData").mockResolvedValueOnce([ + { + id: mockOrgId, + name: "Org Name, LLC", + productTierType: 3, + }, + ]); + + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.ChangePassword, + tab, + domain: "example.com", + newPassword: "newPassword", + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + id: mockCipherId, + organizationId: mockOrgId, + login: { username: "testUser" }, + }); + getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); + + sendMockExtensionMessage(message, sender); + await flushPromises(); + + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + expect(updatePasswordSpy).toHaveBeenCalledWith( + cipherView, + queueMessage.newPassword, + message.edit, + sender.tab, + mockCipherId, + ); + expect(updateWithServerSpy).toHaveBeenCalled(); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "saveCipherAttemptCompleted", + { + cipherId: "testId", + task: { + orgName: "Org Name, LLC", + remainingTasksCount: 1, + }, + username: "testUser", + }, + ); + }); + it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => { const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); const sender = mock({ tab }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index c2e90460dfc..6589252d94b 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, switchMap, map } 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"; @@ -16,27 +16,37 @@ import { } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { ProductTierType } from "@bitwarden/common/billing/enums/product-tier-type.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { TaskService } from "@bitwarden/common/vault/tasks"; +import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums"; +import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; -import { NotificationCipherData } from "../content/components/cipher/types"; +import { + OrganizationCategory, + OrganizationCategories, + NotificationCipherData, +} from "../content/components/cipher/types"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; @@ -70,6 +80,8 @@ export default class NotificationBackground { bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender), bgCloseNotificationBar: ({ message, sender }) => this.handleCloseNotificationBarMessage(message, sender), + bgOpenAtRisksPasswords: ({ message, sender }) => + this.handleOpenAtRisksPasswordsMessage(message, sender), bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), bgGetDecryptedCiphers: () => this.getNotificationCipherData(), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), @@ -106,6 +118,8 @@ export default class NotificationBackground { private policyService: PolicyService, private themeStateService: ThemeStateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, + private taskService: TaskService, + protected messagingService: MessagingService, ) {} init() { @@ -154,23 +168,48 @@ export default class NotificationBackground { firstValueFrom(this.domainSettingsService.showFavicons$), firstValueFrom(this.environmentService.environment$), ]); + const iconsServerUrl = env.getIconsUrl(); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getOptionalUserId), ); + const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl( - currentTab.url, + currentTab?.url, activeUserId, ); + const organizations = await firstValueFrom( + this.organizationService.organizations$(activeUserId), + ); + return decryptedCiphers.map((view) => { - const { id, name, reprompt, favorite, login } = view; + const { id, name, reprompt, favorite, login, organizationId } = view; + + const organizationType = organizationId + ? organizations.find((org) => org.id === organizationId)?.productTierType + : null; + + const organizationCategories: OrganizationCategory[] = []; + + if ( + [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( + organizationType, + ) + ) { + organizationCategories.push(OrganizationCategories.business); + } + if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { + organizationCategories.push(OrganizationCategories.family); + } + return { id, name, type: CipherType.Login, reprompt, favorite, + ...(organizationCategories.length ? { organizationCategories } : {}), icon: buildCipherIcon(iconsServerUrl, view, showFavicons), login: login && { username: login.username, @@ -599,13 +638,13 @@ export default class NotificationBackground { try { await this.cipherService.createWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: String(queueMessage?.username), - cipherId: String(cipher?.id), + username: queueMessage?.username && String(queueMessage.username), + cipherId: cipher?.id && String(cipher.id), }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); } catch (error) { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - error: String(error.message), + error: error?.message && String(error.message), }); } } @@ -638,15 +677,49 @@ export default class NotificationBackground { return; } const cipher = await this.cipherService.encrypt(cipherView, userId); + + const shouldGetTasks = await this.getNotificationFlag(); + try { + const tasks = shouldGetTasks ? await this.getSecurityTasks(userId) : []; + const updatedCipherTask = tasks.find((task) => task.cipherId === cipherView?.id); + const cipherHasTask = !!updatedCipherTask?.id; + + let taskOrgName: string; + if (cipherHasTask && updatedCipherTask?.organizationId) { + const userOrgs = await this.getOrgData(); + taskOrgName = userOrgs.find(({ id }) => id === updatedCipherTask.organizationId)?.name; + } + + const taskData = cipherHasTask + ? { + remainingTasksCount: tasks.length - 1, + orgName: taskOrgName, + } + : undefined; + await this.cipherService.updateWithServer(cipher); + await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: String(cipherView?.login?.username), - cipherId: String(cipherView?.id), + username: cipherView?.login?.username && String(cipherView.login.username), + cipherId: cipherView?.id && String(cipherView.id), + task: taskData, }); + + // If the cipher had a security task, mark it as complete + if (cipherHasTask) { + // guard against multiple (redundant) security tasks per cipher + await Promise.all( + tasks.map((task) => { + if (task.cipherId === cipherView?.id) { + return this.taskService.markAsComplete(task.id, userId); + } + }), + ); + } } catch (error) { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - error: String(error?.message), + error: error?.message && String(error.message), }); } } @@ -699,6 +772,32 @@ export default class NotificationBackground { return null; } + private async getSecurityTasks(userId: UserId) { + let tasks: SecurityTask[] = []; + + if (userId) { + tasks = await firstValueFrom( + this.taskService.tasksEnabled$(userId).pipe( + switchMap((tasksEnabled) => { + if (!tasksEnabled) { + return []; + } + + return this.taskService + .pendingTasks$(userId) + .pipe( + map((tasks) => + tasks.filter(({ type }) => type === SecurityTaskType.UpdateAtRiskCredential), + ), + ); + }), + ), + ); + } + + return tasks; + } + /** * Saves the current tab's domain to the never save list. * @@ -819,6 +918,41 @@ export default class NotificationBackground { }); } + /** + * Sends a message to the background to open the + * at-risk passwords extension view. Triggers + * notification closure as a side-effect. + * + * @param message - The extension message + * @param sender - The contextual sender of the message + */ + private async handleOpenAtRisksPasswordsMessage( + message: NotificationBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + const browserAction = BrowserApi.getBrowserAction(); + + try { + // Set route of the popup before attempting to open it. + // If the vault is locked, this won't have an effect as the auth guards will + // redirect the user to the login page. + await browserAction.setPopup({ popup: "popup/index.html#/at-risk-passwords" }); + + await Promise.all([ + this.messagingService.send(VaultMessages.OpenAtRiskPasswords), + BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }), + ]); + } finally { + // Reset the popup route to the default route so any subsequent + // popup openings will not open to the at-risk-passwords page. + await browserAction.setPopup({ + popup: "popup/index.html#/", + }); + } + } + /** * Sends a message back to the sender tab which triggers * an CSS adjustment of the notification bar. diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 3085dbc2f8d..0fe4a459048 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -117,7 +117,6 @@ describe("OverlayBackground", () => { let getFrameDetailsSpy: jest.SpyInstance; let tabsSendMessageSpy: jest.SpyInstance; let tabSendMessageDataSpy: jest.SpyInstance; - let sendMessageSpy: jest.SpyInstance; let getTabFromCurrentWindowIdSpy: jest.SpyInstance; let getTabSpy: jest.SpyInstance; let openUnlockPopoutSpy: jest.SpyInstance; @@ -228,7 +227,6 @@ describe("OverlayBackground", () => { tabSendMessageDataSpy = jest .spyOn(BrowserApi, "tabSendMessageData") .mockImplementation(() => Promise.resolve()); - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); getTabSpy = jest.spyOn(BrowserApi, "getTab"); openUnlockPopoutSpy = jest.spyOn(overlayBackground as any, "openUnlockPopout"); @@ -1553,7 +1551,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); }); @@ -1579,7 +1576,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); }); @@ -1618,7 +1614,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index fa0ae9b9b3e..4e2e773a0c7 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1852,7 +1852,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { /** * Verifies whether the save login inline menu view should be shown. This requires that - * the login data on the page contains a username and either a current or new password. + * the login data on the page contains either a current or new password. * * @param tab - The tab to check for login data */ @@ -1869,7 +1869,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return ( (this.shouldShowInlineMenuAccountCreation() || this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration)) && - !!(loginData.username && (loginData.password || loginData.newPassword)) + !!(loginData.password || loginData.newPassword) ); } @@ -2157,7 +2157,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { "passwordRegenerated", "passwords", "regeneratePassword", - "saveLoginToBitwarden", + "saveToBitwarden", "toggleBitwardenVaultOverlay", "totpCodeAria", "totpSecondsSpanAria", @@ -2434,7 +2434,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { cipherId: cipherView.id, cipherType: addNewCipherType, }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } catch (error) { this.logService.error("Error building cipher and opening add/edit vault item popout", error); } diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index b07e06234d3..c093f1a3b00 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -1,7 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - import MainBackground from "../../background/main.background"; import { OverlayBackground } from "./abstractions/overlay.background"; @@ -14,7 +10,7 @@ export default class TabsBackground { private overlayBackground: OverlayBackground, ) {} - private focusedWindowId: number; + private focusedWindowId: number = -1; /** * Initializes the window and tab listeners. @@ -90,14 +86,6 @@ export default class TabsBackground { changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ) => { - const overlayImprovementsFlag = await this.main.configService.getFeatureFlag( - FeatureFlag.InlineMenuPositioningImprovements, - ); - const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { - this.overlayBackground.removePageDetails(tabId); - } - if (this.focusedWindowId > 0 && tab.windowId !== this.focusedWindowId) { return; } diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index f0642d4233a..881b44b4785 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -11,7 +11,7 @@ export function ActionButton({ theme, handleClick, }: { - buttonText: string; + buttonText: string | TemplateResult; disabled?: boolean; theme: Theme; handleClick: (e: Event) => void; @@ -63,4 +63,9 @@ const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: The color: ${themes[theme].text.contrast}; } `} + + svg { + width: fit-content; + height: 16px; + } `; diff --git a/apps/browser/src/autofill/content/components/buttons/close-button.ts b/apps/browser/src/autofill/content/components/buttons/close-button.ts index c32d0c130e3..05a12d4f453 100644 --- a/apps/browser/src/autofill/content/components/buttons/close-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/close-button.ts @@ -35,5 +35,6 @@ const closeButtonStyles = (theme: Theme) => css` > svg { width: 20px; height: 20px; + vertical-align: middle; } `; diff --git a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts index cf9a561ee39..e3c7e0d54e6 100644 --- a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts @@ -44,7 +44,7 @@ export function OptionSelectionButton({ `; } -const iconSize = "15px"; +const iconSize = "16px"; const selectionButtonStyles = ({ disabled, @@ -94,7 +94,8 @@ const selectionButtonStyles = ({ > svg { max-width: ${iconSize}; - height: fit-content; + max-height: ${iconSize}; + height: auto; } `; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index 2d386d34d6a..aaa4b11d8a2 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -19,13 +19,13 @@ export function CipherAction({ ? BadgeButton({ buttonAction: handleAction, // @TODO localize - buttonText: "Update item", + buttonText: "Update", theme, }) : EditButton({ buttonAction: handleAction, // @TODO localize - buttonText: "Edit item", + buttonText: "Edit", theme, }); } diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index 39d4dd28f24..5e78fd5658a 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -1,30 +1,35 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; import { Business, Family } from "../../../content/components/icons"; -// @TODO connect data source to icon checks -// @TODO support other indicator types (attachments, etc) +import { OrganizationCategories, OrganizationCategory } from "./types"; + +const cipherIndicatorIconsMap: Record< + OrganizationCategory, + (args: { color: string; theme: Theme }) => TemplateResult +> = { + [OrganizationCategories.business]: Business, + [OrganizationCategories.family]: Family, +}; + export function CipherInfoIndicatorIcons({ - showBusinessIcon, - showFamilyIcon, + organizationCategories = [], theme, }: { - showBusinessIcon?: boolean; - showFamilyIcon?: boolean; + organizationCategories?: OrganizationCategory[]; theme: Theme; }) { - const indicatorIcons = [ - ...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []), - ...(showFamilyIcon ? [Family({ color: themes[theme].text.muted, theme })] : []), - ]; - - return indicatorIcons.length - ? html` ${indicatorIcons} ` - : null; // @TODO null case should be handled by parent + return html` + + ${organizationCategories.map((name) => + cipherIndicatorIconsMap[name]?.({ color: themes[theme].text.muted, theme }), + )} + + `; } const cipherInfoIndicatorIconsStyles = css` diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts index 6ff32353938..e3d237b9bc6 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -8,14 +8,22 @@ import { themes, typography } from "../../../content/components/constants/styles import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons"; import { NotificationCipherData } from "./types"; -// @TODO support other cipher types (card, identity, notes, etc) export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) { - const { name, login } = cipher; + const { name, login, organizationCategories } = cipher; + const hasIndicatorIcons = organizationCategories?.length; return html`
- ${[name, CipherInfoIndicatorIcons({ theme })]} + ${[ + name, + hasIndicatorIcons + ? CipherInfoIndicatorIcons({ + theme, + organizationCategories, + }) + : nothing, + ]} ${login?.username diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts index ff29f9b559f..590311682bf 100644 --- a/apps/browser/src/autofill/content/components/cipher/types.ts +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -14,6 +14,14 @@ export const CipherRepromptTypes = { type CipherRepromptType = (typeof CipherRepromptTypes)[keyof typeof CipherRepromptTypes]; +export type OrganizationCategory = + (typeof OrganizationCategories)[keyof typeof OrganizationCategories]; + +export const OrganizationCategories = { + business: "business", + family: "family", +} as const; + export type WebsiteIconData = { imageEnabled: boolean; image: string; @@ -50,4 +58,5 @@ export type NotificationCipherData = BaseCipherData & login?: { username: string; }; + organizationCategories?: OrganizationCategory[]; }; diff --git a/apps/browser/src/autofill/content/components/icons/angle-down.ts b/apps/browser/src/autofill/content/components/icons/angle-down.ts index db5275aafa9..27cd5ab81c5 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-down.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-down.ts @@ -8,10 +8,10 @@ export function AngleDown({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/angle-up.ts b/apps/browser/src/autofill/content/components/icons/angle-up.ts index 7344123d5ad..f8bda632285 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-up.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-up.ts @@ -8,15 +8,10 @@ export function AngleUp({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts index 8df68d79b6e..1b08f261eb6 100644 --- a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts +++ b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts @@ -12,8 +12,13 @@ export function BrandIconContainer({ iconLink, theme }: { iconLink?: URL; theme: } const brandIconContainerStyles = css` + display: flex; + justify-content: center; + width: 24px; + height: 24px; + > svg { - width: 20px; - height: fit-content; + width: auto; + height: 100%; } `; diff --git a/apps/browser/src/autofill/content/components/icons/business.ts b/apps/browser/src/autofill/content/components/icons/business.ts index ef8e082c21f..79e64a0a1f9 100644 --- a/apps/browser/src/autofill/content/components/icons/business.ts +++ b/apps/browser/src/autofill/content/components/icons/business.ts @@ -8,30 +8,17 @@ export function Business({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + - - `; } diff --git a/apps/browser/src/autofill/content/components/icons/close.ts b/apps/browser/src/autofill/content/components/icons/close.ts index c9d9286ca3f..27610bc7773 100644 --- a/apps/browser/src/autofill/content/components/icons/close.ts +++ b/apps/browser/src/autofill/content/components/icons/close.ts @@ -8,10 +8,10 @@ export function Close({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/collection-shared.ts b/apps/browser/src/autofill/content/components/icons/collection-shared.ts new file mode 100644 index 00000000000..de366b88e92 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/collection-shared.ts @@ -0,0 +1,23 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function CollectionShared({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts index d87d5621e30..c4f587b2d7b 100644 --- a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts +++ b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts @@ -8,10 +8,20 @@ export function ExclamationTriangle({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + + `; diff --git a/apps/browser/src/autofill/content/components/icons/external-link.ts b/apps/browser/src/autofill/content/components/icons/external-link.ts new file mode 100644 index 00000000000..10c6d831025 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/external-link.ts @@ -0,0 +1,22 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function ExternalLink({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/family.ts b/apps/browser/src/autofill/content/components/icons/family.ts index 9870c5d37c0..4a9db91f43a 100644 --- a/apps/browser/src/autofill/content/components/icons/family.ts +++ b/apps/browser/src/autofill/content/components/icons/family.ts @@ -8,10 +8,11 @@ export function Family({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/folder.ts b/apps/browser/src/autofill/content/components/icons/folder.ts index 84577aef820..1b93d2d32ea 100644 --- a/apps/browser/src/autofill/content/components/icons/folder.ts +++ b/apps/browser/src/autofill/content/components/icons/folder.ts @@ -8,10 +8,11 @@ export function Folder({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/globe.ts b/apps/browser/src/autofill/content/components/icons/globe.ts index fc0a975284d..936fd8d2802 100644 --- a/apps/browser/src/autofill/content/components/icons/globe.ts +++ b/apps/browser/src/autofill/content/components/icons/globe.ts @@ -8,11 +8,10 @@ export function Globe({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index c4769a0e69d..65ec6301ac4 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -3,12 +3,12 @@ export { AngleUp } from "./angle-up"; export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; +export { CollectionShared } from "./collection-shared"; export { ExclamationTriangle } from "./exclamation-triangle"; +export { ExternalLink } from "./external-link"; export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; -export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; export { User } from "./user"; -export { Warning } from "./warning"; diff --git a/apps/browser/src/autofill/content/components/icons/pencil-square.ts b/apps/browser/src/autofill/content/components/icons/pencil-square.ts index f41ab927809..11366f2631a 100644 --- a/apps/browser/src/autofill/content/components/icons/pencil-square.ts +++ b/apps/browser/src/autofill/content/components/icons/pencil-square.ts @@ -8,10 +8,14 @@ export function PencilSquare({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + `; diff --git a/apps/browser/src/autofill/content/components/icons/shield.ts b/apps/browser/src/autofill/content/components/icons/shield.ts index 5a2d7d39d58..a027dd3e113 100644 --- a/apps/browser/src/autofill/content/components/icons/shield.ts +++ b/apps/browser/src/autofill/content/components/icons/shield.ts @@ -8,10 +8,10 @@ export function Shield({ color, theme }: IconProps) { const shapeColor = color || themes[theme].brandLogo; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/user.ts b/apps/browser/src/autofill/content/components/icons/user.ts index 32ccd3a2031..b59204a0ad8 100644 --- a/apps/browser/src/autofill/content/components/icons/user.ts +++ b/apps/browser/src/autofill/content/components/icons/user.ts @@ -8,10 +8,10 @@ export function User({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/warning.ts b/apps/browser/src/autofill/content/components/icons/warning.ts deleted file mode 100644 index 9ae9aeca352..00000000000 --- a/apps/browser/src/autofill/content/components/icons/warning.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { html } from "lit"; - -// This icon has static multi-colors for each theme -export function Warning() { - return html` - - - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/illustrations/celebrate.ts similarity index 98% rename from apps/browser/src/autofill/content/components/icons/party-horn.ts rename to apps/browser/src/autofill/content/components/illustrations/celebrate.ts index 439d60a79de..30b3743004f 100644 --- a/apps/browser/src/autofill/content/components/icons/party-horn.ts +++ b/apps/browser/src/autofill/content/components/illustrations/celebrate.ts @@ -5,16 +5,10 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { IconProps } from "../common-types"; // This icon has static multi-colors for each theme -export function PartyHorn({ theme }: IconProps) { +export function Celebrate({ theme }: IconProps) { if (theme === ThemeTypes.Dark) { return html` - + + + + + + + + + + + + + + + + + + + + + + + `; + } + + return html` + + + + + + + + + + + + + + + + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/illustrations/warning.ts b/apps/browser/src/autofill/content/components/illustrations/warning.ts new file mode 100644 index 00000000000..4e15a09839a --- /dev/null +++ b/apps/browser/src/autofill/content/components/illustrations/warning.ts @@ -0,0 +1,22 @@ +import { html } from "lit"; + +// This icon has static multi-colors for each theme +export function Warning() { + return html` + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx index f85cf3ae90f..571ed10285a 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx @@ -2,7 +2,7 @@ import { Meta, Controls } from "@storybook/addon-docs"; import * as stories from "./icons.lit-stories"; - + ## Icon Stories @@ -14,12 +14,12 @@ like size, color, and theme. Each story is an example of how a specific icon can | | | | ------------------------- | ------------------ | -| `AngleDownIcon` | `FolderIcon` | -| `BusinessIcon` | `GlobeIcon` | -| `BrandIcon` | `PartyHornIcon` | +| `AngleDownIcon` | `AngleUpIcon` | +| `BusinessIcon` | `FolderIcon` | +| `BrandIcon` | `GlobeIcon` | | `CloseIcon` | `PencilSquareIcon` | | `ExclamationTriangleIcon` | `ShieldIcon` | -| `FamilyIcon` | `UserIcon` | +| `UsersIcon` | `UserIcon` | ## Props 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-icons.lit-stories.ts similarity index 94% rename from apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts index 89c3ecbcb1c..08530452730 100644 --- 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-icons.lit-stories.ts @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Ciphers/Cipher Indicator Icon", + title: "Components/Ciphers/Cipher Indicator Icons", argTypes: { showBusinessIcon: { control: "boolean" }, showFamilyIcon: { control: "boolean" }, 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 index 20c88a59246..c74895e1dea 100644 --- 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 @@ -14,7 +14,7 @@ type Args = { }; export default { - title: "Components/Icons/Icons", + title: "Components/Icons", argTypes: { iconLink: { control: "text" }, color: { control: "color" }, @@ -43,24 +43,23 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj => { render: (args) => Template(args, Icons[iconName]), } as StoryObj; - if (iconName !== "BrandIconContainer") { - story.argTypes = { - iconLink: { table: { disable: true } }, - }; - } + story.argTypes = { + iconLink: { table: { disable: true } }, + }; return story; }; export const AngleDownIcon = createIconStory("AngleDown"); +export const AngleUpIcon = createIconStory("AngleUp"); export const BusinessIcon = createIconStory("Business"); -export const BrandIcon = createIconStory("BrandIconContainer"); export const CloseIcon = createIconStory("Close"); +export const CollectionSharedIcon = createIconStory("CollectionShared"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); +export const ExternalLinkIcon = createIconStory("ExternalLink"); 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/illustrations/illustrations.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts new file mode 100644 index 00000000000..86d55f2f795 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts @@ -0,0 +1,44 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import * as Illustrations from "../../illustrations"; + +type Args = { + theme: Theme; + size: number; +}; + +export default { + title: "Components/Illustrations", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + size: { control: "number", min: 10, max: 100, step: 1 }, + }, + args: { + theme: ThemeTypes.Light, + size: 50, + }, +} as Meta; + +const Template = ( + args: Args, + IllustrationComponent: (props: Args) => ReturnType, +) => html` +
+ ${IllustrationComponent({ ...args })} +
+`; + +const createIllustrationStory = (illustrationName: keyof typeof Illustrations): StoryObj => { + return { + render: (args) => Template(args, Illustrations[illustrationName]), + } as StoryObj; +}; + +export const KeyholeIllustration = createIllustrationStory("Keyhole"); +export const CelebrateIllustration = createIllustrationStory("Celebrate"); +export const WarningIllustration = createIllustrationStory("Warning"); 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 index e43bc08b920..32b4170d1da 100644 --- 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 @@ -16,7 +16,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Body", + title: "Components/Notifications/Body", argTypes: { ciphers: { control: "object" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts similarity index 53% rename from apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts index b3dee95efd0..4d9be06fd7e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts @@ -1,29 +1,26 @@ import { Meta, StoryObj } from "@storybook/web-components"; -import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { NotificationConfirmationBody } from "../../notification/confirmation"; - -type Args = { - buttonText: string; - confirmationMessage: string; - handleOpenVault: () => void; - theme: Theme; - error: string; -}; +import { + NotificationConfirmationBody, + NotificationConfirmationBodyProps, +} from "../../../notification/confirmation/body"; export default { - title: "Components/Notifications/Notification Confirmation Body", + title: "Components/Notifications/Confirmation/Body", argTypes: { error: { control: "text" }, buttonText: { control: "text" }, confirmationMessage: { control: "text" }, + messageDetails: { control: "text" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, }, args: { error: "", buttonText: "View", confirmationMessage: "[item name] updated in Bitwarden.", + messageDetails: "You can view it in your vault.", theme: ThemeTypes.Light, }, parameters: { @@ -32,10 +29,11 @@ export default { url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", }, }, -} as Meta; +} as Meta; -const Template = (args: Args) => NotificationConfirmationBody({ ...args }); +const Template = (args: NotificationConfirmationBodyProps) => + NotificationConfirmationBody({ ...args }); -export const Default: StoryObj = { +export const Default: StoryObj = { render: Template, }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts new file mode 100644 index 00000000000..ec7194004d8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts @@ -0,0 +1,55 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar"; +import { + NotificationConfirmationContainer, + NotificationConfirmationContainerProps, +} from "../../../notification/confirmation/container"; + +export default { + title: "Components/Notifications/Confirmation", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [NotificationTypes.Change, NotificationTypes.Add] }, + }, + args: { + error: "", + task: { + orgName: "Acme, Inc.", + remainingTasksCount: 0, + }, + handleCloseNotification: () => alert("Close notification action triggered"), + handleOpenTasks: () => alert("Open tasks action triggered"), + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + loginUpdateTaskSuccessAdditional: + "Thank you for making your organization more secure. You have 3 more passwords to update.", + loginUpdateTaskSuccess: + "Great job! You took the steps to make you and your organization more secure.", + nextSecurityTaskAction: "Change next password", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + view: "View", + }, + type: NotificationTypes.Change, + username: "Acme, Inc. Login", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationContainerProps) => + NotificationConfirmationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts new file mode 100644 index 00000000000..953fb90d067 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { + NotificationConfirmationFooter, + NotificationConfirmationFooterProps, +} from "../../../notification/confirmation/footer"; + +export default { + title: "Components/Notifications/Confirmation/Footer", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + handleButtonClick: () => alert("Action button triggered"), + i18n: { + nextSecurityTaskAction: "Change next password", + }, + theme: ThemeTypes.Light, + }, + 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: NotificationConfirmationFooterProps) => + html`
${NotificationConfirmationFooter({ ...args })}
`; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts new file mode 100644 index 00000000000..f01503b331f --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { + NotificationConfirmationMessage, + NotificationConfirmationMessageProps, +} from "../../../notification/confirmation/message"; + +export default { + title: "Components/Notifications/Confirmation/Message", + argTypes: { + buttonText: { control: "text" }, + message: { control: "text" }, + messageDetails: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + buttonText: "View", + message: "[item name] updated in Bitwarden.", + messageDetails: "It was added to your vault.", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationMessageProps) => + NotificationConfirmationMessage({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts new file mode 100644 index 00000000000..351c971ec0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts @@ -0,0 +1,61 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; +import { NotificationContainer, NotificationContainerProps } from "../../notification/container"; + +export default { + title: "Components/Notifications", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [...Object.values(NotificationTypes)] }, + }, + args: { + error: "", + 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" }, + }, + ], + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + saveAction: "Save", + saveAsNewLoginAction: "Save as new login", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + updateLoginPrompt: "Update existing login?", + view: "View", + }, + type: NotificationTypes.Change, + username: "mockUsername", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationContainerProps) => NotificationContainer({ ...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 index ea2bbdc2e15..29d9955ec64 100644 --- 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 @@ -7,7 +7,7 @@ import { NotificationFooter, NotificationFooterProps } from "../../notification/ import { mockFolderData, mockOrganizationData } from "../mock-data"; export default { - title: "Components/Notifications/Notification Footer", + title: "Components/Notifications/Footer", argTypes: { notificationType: { control: "select", 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 index 49cc1e6bd8d..0857c99130e 100644 --- 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 @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Header", + title: "Components/Notifications/Header", argTypes: { message: { control: "text" }, standalone: { control: "boolean" }, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts b/apps/browser/src/autofill/content/components/notification/confirmation-message.ts deleted file mode 100644 index d6f7ba3024d..00000000000 --- a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { Theme } from "@bitwarden/common/platform/enums"; - -import { themes } from "../constants/styles"; - -export function NotificationConfirmationMessage({ - buttonText, - confirmationMessage, - handleClick, - theme, -}: { - buttonText: string; - confirmationMessage: string; - handleClick: (e: Event) => void; - theme: Theme; -}) { - return html` - ${confirmationMessage} - ${buttonText} - `; -} - -const baseTextStyles = css` - flex-grow: 1; - overflow-x: hidden; - text-align: left; - text-overflow: ellipsis; - line-height: 24px; - font-family: "DM Sans", sans-serif; - font-size: 16px; -`; - -const notificationConfirmationMessageStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].text.main}; - font-weight: 400; -`; - -const notificationConfirmationButtonTextStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].primary[600]}; - font-weight: 700; - cursor: pointer; -`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts similarity index 63% rename from apps/browser/src/autofill/content/components/notification/confirmation.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 8c213a7663f..d2ac7f36277 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -1,12 +1,12 @@ import createEmotion from "@emotion/css/create-instance"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; -import { themes } from "../constants/styles"; -import { PartyHorn, Warning } from "../icons"; +import { themes } from "../../constants/styles"; +import { Celebrate, Keyhole, Warning } from "../../illustrations"; -import { NotificationConfirmationMessage } from "./confirmation-message"; +import { NotificationConfirmationMessage } from "./message"; export const componentClassPrefix = "notification-confirmation-body"; @@ -14,31 +14,41 @@ const { css } = createEmotion({ key: componentClassPrefix, }); -export function NotificationConfirmationBody({ - buttonText, - error, - confirmationMessage, - theme, - handleOpenVault, -}: { - error?: string; +export type NotificationConfirmationBodyProps = { buttonText: string; confirmationMessage: string; + error?: string; + messageDetails?: string; + tasksAreComplete?: boolean; theme: Theme; handleOpenVault: (e: Event) => void; -}) { - const IconComponent = !error ? PartyHorn : Warning; +}; + +export function NotificationConfirmationBody({ + buttonText, + confirmationMessage, + error, + messageDetails, + tasksAreComplete, + theme, + handleOpenVault, +}: NotificationConfirmationBodyProps) { + const IconComponent = tasksAreComplete ? Keyhole : !error ? Celebrate : Warning; + + const showConfirmationMessage = confirmationMessage || buttonText || messageDetails; + return html`
${IconComponent({ theme })}
- ${confirmationMessage && buttonText + ${showConfirmationMessage ? NotificationConfirmationMessage({ - handleClick: handleOpenVault, - confirmationMessage, - theme, buttonText, + message: confirmationMessage, + messageDetails, + theme, + handleClick: handleOpenVault, }) - : null} + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts similarity index 66% rename from apps/browser/src/autofill/content/components/notification/confirmation-container.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/container.ts index 0666859ac44..a071338af9a 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -1,42 +1,67 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { NotificationBarIframeInitData, - NotificationTypes, + NotificationTaskInfo, NotificationType, -} from "../../../notification/abstractions/notification-bar"; -import { themes, spacing } from "../constants/styles"; - -import { NotificationConfirmationBody } from "./confirmation"; + NotificationTypes, +} from "../../../../notification/abstractions/notification-bar"; +import { themes, spacing } from "../../constants/styles"; import { NotificationHeader, componentClassPrefix as notificationHeaderClassPrefix, -} from "./header"; +} from "../header"; + +import { NotificationConfirmationBody } from "./body"; +import { NotificationConfirmationFooter } from "./footer"; + +export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleOpenVault: (e: Event) => void; + handleOpenTasks: (e: Event) => void; +} & { + error?: string; + i18n: { [key: string]: string }; + task?: NotificationTaskInfo; + type: NotificationType; + username: string; +}; export function NotificationConfirmationContainer({ error, handleCloseNotification, handleOpenVault, + handleOpenTasks, i18n, + task, theme = ThemeTypes.Light, type, username, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; -} & { - error?: string; - i18n: { [key: string]: string }; - type: NotificationType; - username: string; -}) { +}: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); const confirmationMessage = getConfirmationMessage(i18n, username, type, error); const buttonText = error ? i18n.newItem : i18n.view; + let messageDetails: string | undefined; + let remainingTasksCount: number | undefined; + let tasksAreComplete: boolean = false; + + if (task) { + remainingTasksCount = task.remainingTasksCount || 0; + tasksAreComplete = remainingTasksCount === 0; + + messageDetails = + remainingTasksCount > 0 + ? chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional", [ + task.orgName, + `${remainingTasksCount}`, + ]) + : chrome.i18n.getMessage("loginUpdateTaskSuccess", [task.orgName]); + } + return html`
${NotificationHeader({ @@ -47,10 +72,18 @@ export function NotificationConfirmationContainer({ ${NotificationConfirmationBody({ buttonText, confirmationMessage, - error: error, - handleOpenVault, + tasksAreComplete, + messageDetails, theme, + handleOpenVault, })} + ${remainingTasksCount + ? NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick: handleOpenTasks, + }) + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts new file mode 100644 index 00000000000..e245d22c8e8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts @@ -0,0 +1,59 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { ActionButton } from "../../buttons/action-button"; +import { spacing, themes } from "../../constants/styles"; +import { ExternalLink } from "../../icons"; + +export type NotificationConfirmationFooterProps = { + i18n: { [key: string]: string }; + theme: Theme; + handleButtonClick: (event: Event) => void; +}; + +export function NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick, +}: NotificationConfirmationFooterProps) { + const primaryButtonText = i18n.nextSecurityTaskAction; + + return html` +
+ ${ActionButton({ + handleClick: handleButtonClick, + buttonText: AdditionalTasksButtonContent({ buttonText: primaryButtonText, theme }), + theme, + })} +
+ `; +} + +const notificationConfirmationFooterStyles = ({ theme }: { theme: Theme }) => css` + background-color: ${themes[theme].background.alt}; + padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + max-width: min-content; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + padding-bottom: ${spacing[4]}; + } +`; + +function AdditionalTasksButtonContent({ buttonText, theme }: { buttonText: string; theme: Theme }) { + return html` +
+ ${buttonText} + ${ExternalLink({ theme, color: themes[theme].text.contrast })} +
+ `; +} + +const additionalTasksButtonContentStyles = ({ theme }: { theme: Theme }) => css` + gap: ${spacing[2]}; + display: flex; + align-items: center; + white-space: nowrap; +`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts new file mode 100644 index 00000000000..c018371caff --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -0,0 +1,83 @@ +import { css } from "@emotion/css"; +import { html, nothing } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography } from "../../constants/styles"; + +export type NotificationConfirmationMessageProps = { + buttonText?: string; + message?: string; + messageDetails?: string; + handleClick: (e: Event) => void; + theme: Theme; +}; + +export function NotificationConfirmationMessage({ + buttonText, + message, + messageDetails, + handleClick, + theme, +}: NotificationConfirmationMessageProps) { + return html` +
+ ${message || buttonText + ? html` + + ${message || nothing} + ${buttonText + ? html` + + ${buttonText} + + ` + : nothing} + + ` + : nothing} + ${messageDetails + ? html`
${messageDetails}
` + : nothing} +
+ `; +} + +const baseTextStyles = css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 24px; + font-family: "DM Sans", sans-serif; + font-size: 16px; +`; + +const notificationConfirmationMessageStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].text.main}; + font-weight: 400; +`; + +const notificationConfirmationButtonTextStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].primary[600]}; + font-weight: 700; + cursor: pointer; +`; + +const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body2} + + font-size: 14px; + color: ${themes[theme].text.muted}; +`; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 8d80dc9fb50..c29f58e116b 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -19,6 +19,18 @@ import { componentClassPrefix as notificationHeaderClassPrefix, } from "./header"; +export type NotificationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleSaveAction: (e: Event) => void; + handleEditOrUpdateAction: (e: Event) => void; +} & { + ciphers?: NotificationCipherData[]; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; + type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` +}; + export function NotificationContainer({ handleCloseNotification, handleEditOrUpdateAction, @@ -29,17 +41,7 @@ export function NotificationContainer({ organizations, theme = ThemeTypes.Light, type, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleSaveAction: (e: Event) => void; - handleEditOrUpdateAction: (e: Event) => void; -} & { - ciphers?: NotificationCipherData[]; - folders?: FolderView[]; - i18n: { [key: string]: string }; - organizations?: OrgView[]; - type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` -}) { +}: NotificationContainerProps) { const headerMessage = getHeaderMessage(i18n, type); const showBody = true; diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts index 50c2c629942..d6cedf6a85a 100644 --- a/apps/browser/src/autofill/content/components/notification/header.ts +++ b/apps/browser/src/autofill/content/components/notification/header.ts @@ -49,7 +49,7 @@ const notificationHeaderStyles = ({ display: flex; align-items: center; justify-content: flex-start; - background-color: ${themes[theme].background}; + background-color: ${themes[theme].background.DEFAULT}; padding: 12px 16px 8px 16px; white-space: nowrap; diff --git a/apps/browser/src/autofill/content/components/option-selection/option-item.ts b/apps/browser/src/autofill/content/components/option-selection/option-item.ts index 619d77e63d3..e8a293e2c3f 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-item.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-item.ts @@ -62,14 +62,15 @@ const optionItemStyles = css` `; const optionItemIconContainerStyles = css` + display: flex; flex-grow: 1; flex-shrink: 1; - width: ${optionItemIconWidth}px; - height: ${optionItemIconWidth}px; + max-width: ${optionItemIconWidth}px; + max-height: ${optionItemIconWidth}px; > svg { width: 100%; - height: fit-content; + height: auto; } `; diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts deleted file mode 100644 index 88b78dc2495..00000000000 --- a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; - -import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background"; -import AutofillPageDetails from "../../../models/autofill-page-details"; - -type WebsiteIconData = { - imageEnabled: boolean; - image: string; - fallbackImage: string; - icon: string; -}; - -type OverlayAddNewItemMessage = { - login?: { - uri?: string; - hostname: string; - username: string; - password: string; - }; -}; - -type OverlayBackgroundExtensionMessage = { - [key: string]: any; - command: string; - tab?: chrome.tabs.Tab; - sender?: string; - details?: AutofillPageDetails; - overlayElement?: string; - display?: string; - data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; - -type OverlayPortMessage = { - [key: string]: any; - command: string; - direction?: string; - overlayCipherId?: string; -}; - -type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; -}; - -type OverlayCipherData = { - id: string; - name: string; - type: CipherType; - reprompt: CipherRepromptType; - favorite: boolean; - icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; - login?: { username: string }; - card?: string; -}; - -type BackgroundMessageParam = { - message: OverlayBackgroundExtensionMessage; -}; -type BackgroundSenderParam = { - sender: chrome.runtime.MessageSender; -}; -type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; - -type OverlayBackgroundExtensionMessageHandlers = { - [key: string]: CallableFunction; - openAutofillOverlay: () => void; - autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - getAutofillOverlayVisibility: () => void; - checkAutofillOverlayFocused: () => void; - focusAutofillOverlayList: () => void; - updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; - updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - unlockCompleted: ({ message }: BackgroundMessageParam) => void; - addedCipher: () => void; - addEditCipherSubmitted: () => void; - editedCipher: () => void; - deletedCipher: () => void; -}; - -type PortMessageParam = { - message: OverlayPortMessage; -}; -type PortConnectionParam = { - port: chrome.runtime.Port; -}; -type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; - -type OverlayButtonPortMessageHandlers = { - [key: string]: CallableFunction; - overlayButtonClicked: ({ port }: PortConnectionParam) => void; - closeAutofillOverlay: ({ port }: PortConnectionParam) => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; -}; - -type OverlayListPortMessageHandlers = { - [key: string]: CallableFunction; - checkAutofillOverlayButtonFocused: () => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - unlockVault: ({ port }: PortConnectionParam) => void; - fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; - addNewVaultItem: ({ port }: PortConnectionParam) => void; - viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; -}; - -export { - WebsiteIconData, - OverlayBackgroundExtensionMessage, - OverlayPortMessage, - FocusedFieldData, - OverlayCipherData, - OverlayAddNewItemMessage, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayListPortMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts deleted file mode 100644 index 128dd189878..00000000000 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ /dev/null @@ -1,1467 +0,0 @@ -import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { - SHOW_AUTOFILL_BUTTON, - AutofillOverlayVisibility, -} from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { - DefaultDomainSettingsService, - DomainSettingsService, -} from "@bitwarden/common/autofill/services/domain-settings.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { - EnvironmentService, - Region, -} from "@bitwarden/common/platform/abstractions/environment.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { - FakeStateProvider, - FakeAccountService, - mockAccountServiceWith, -} from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -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 { CipherService } from "@bitwarden/common/vault/services/cipher.service"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; -import { - AutofillOverlayElement, - AutofillOverlayPort, - RedirectFocusDirection, -} from "../../enums/autofill-overlay.enum"; -import { AutofillService } from "../../services/abstractions/autofill.service"; -import { - createAutofillPageDetailsMock, - createChromeTabMock, - createFocusedFieldDataMock, - createPageDetailMock, - createPortSpyMock, -} from "../../spec/autofill-mocks"; -import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils"; - -import LegacyOverlayBackground from "./overlay.background.deprecated"; - -describe("OverlayBackground", () => { - const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); - let domainSettingsService: DomainSettingsService; - let buttonPortSpy: chrome.runtime.Port; - let listPortSpy: chrome.runtime.Port; - let overlayBackground: LegacyOverlayBackground; - const cipherService = mock(); - const autofillService = mock(); - let configService: MockProxy; - let activeAccountStatusMock$: BehaviorSubject; - let authService: MockProxy; - - const environmentService = mock(); - environmentService.environment$ = new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ); - const autofillSettingsService = mock(); - const i18nService = mock(); - const platformUtilsService = mock(); - const themeStateService = mock(); - const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { - const { initList, initButton } = options; - if (initButton) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - } - - if (initList) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); - listPortSpy = overlayBackground["overlayListPort"]; - } - - return { buttonPortSpy, listPortSpy }; - }; - - beforeEach(() => { - configService = mock(); - configService.getFeatureFlag$.mockImplementation(() => of(true)); - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); - activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); - authService = mock(); - authService.activeAccountStatus$ = activeAccountStatusMock$; - overlayBackground = new LegacyOverlayBackground( - cipherService, - autofillService, - authService, - environmentService, - domainSettingsService, - autofillSettingsService, - i18nService, - platformUtilsService, - themeStateService, - accountService, - ); - - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - - themeStateService.selectedTheme$ = of(ThemeType.Light); - domainSettingsService.showFavicons$ = of(true); - - void overlayBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - mockReset(cipherService); - }); - - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { - const tabId = 1; - const frameId = 2; - overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); - overlayBackground.removePageDetails(tabId); - - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); - }); - }); - - describe("init", () => { - it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { - overlayBackground["setupExtensionMessageListeners"] = jest.fn(); - overlayBackground["getOverlayVisibility"] = jest.fn(); - overlayBackground["getAuthStatus"] = jest.fn(); - - await overlayBackground.init(); - - expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - }); - }); - - describe("updateOverlayCiphers", () => { - const url = "https://jest-testing-website.com"; - const tab = createChromeTabMock({ url }); - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - - beforeEach(() => { - activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); - }); - - it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { - activeAccountStatusMock$.next(AuthenticationStatus.Locked); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); - }); - - it("ignores updating the overlay ciphers if the tab is undefined", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); - }); - - it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); - cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId); - expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); - expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( - new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ]), - ); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - }); - - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { - overlayBackground["overlayListPort"] = mock(); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); - cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - await overlayBackground.updateOverlayCiphers(); - - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayListCiphers", - ciphers: [ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - ], - }); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - tab, - "updateIsOverlayCiphersPopulated", - { isOverlayCiphersPopulated: true }, - ); - }); - }); - - describe("getOverlayCipherData", () => { - const url = "https://jest-testing-website.com"; - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - const cipher3 = mock({ - id: "id-3", - localData: { lastUsedDate: 333 }, - name: "name-3", - type: CipherType.Card, - card: { subTitle: "Visa, *6789" }, - }); - const cipher4 = mock({ - id: "id-4", - localData: { lastUsedDate: 444 }, - name: "name-4", - type: CipherType.Card, - card: { subTitle: "Mastercard, *1234" }, - }); - - it("formats and returns the cipher data", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher3], - ["overlay-cipher-3", cipher4], - ]); - - const overlayCipherData = await overlayBackground["getOverlayCipherData"](); - - expect(overlayCipherData).toStrictEqual([ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - { - card: "Visa, *6789", - favorite: cipher3.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: null, - imageEnabled: true, - }, - id: "overlay-cipher-2", - login: null, - name: "name-3", - reprompt: cipher3.reprompt, - type: 3, - }, - { - card: "Mastercard, *1234", - favorite: cipher4.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: null, - imageEnabled: true, - }, - id: "overlay-cipher-3", - login: null, - name: "name-4", - reprompt: cipher4.reprompt, - type: 3, - }, - ]); - }); - }); - - describe("getAuthStatus", () => { - it("will update the user's auth status but will not update the overlay ciphers", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - const status = await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - expect(status).toBe(authStatus); - }); - - it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - }); - }); - - describe("updateOverlayButtonAuthStatus", () => { - it("will send a message to the button port with the user's auth status", () => { - overlayBackground["overlayButtonPort"] = mock(); - jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); - - overlayBackground["updateOverlayButtonAuthStatus"](); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayButtonAuthStatus", - authStatus: overlayBackground["userAuthStatus"], - }); - }); - }); - - describe("getTranslations", () => { - it("will query the overlay page translations if they have not been queried", () => { - overlayBackground["overlayPageTranslations"] = undefined; - jest.spyOn(overlayBackground as any, "getTranslations"); - jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); - jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); - - const translations = overlayBackground["getTranslations"](); - - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - const translationKeys = [ - "opensInANewWindow", - "bitwardenOverlayButton", - "toggleBitwardenVaultOverlay", - "bitwardenVault", - "unlockYourAccountToViewMatchingLogins", - "unlockAccount", - "fillCredentialsFor", - "partialUsername", - "view", - "noItemsToShow", - "newItem", - "addNewVaultItem", - ]; - translationKeys.forEach((key) => { - expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); - }); - expect(translations).toStrictEqual({ - locale: "en", - opensInANewWindow: "opensInANewWindow", - buttonPageTitle: "bitwardenOverlayButton", - toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", - listPageTitle: "bitwardenVault", - unlockYourAccount: "unlockYourAccountToViewMatchingLogins", - unlockAccount: "unlockAccount", - fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", - view: "view", - noItemsToShow: "noItemsToShow", - newItem: "newItem", - addNewVaultItem: "addNewVaultItem", - }); - }); - }); - - describe("setupExtensionMessageListeners", () => { - it("will set up onMessage and onConnect listeners", () => { - overlayBackground["setupExtensionMessageListeners"](); - - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); - }); - }); - - describe("handleExtensionMessage", () => { - it("will return early if the message command is not present within the extensionMessageHandlers", () => { - const message = { - command: "not-a-command", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(null); - expect(sendResponse).not.toHaveBeenCalled(); - }); - - it("will trigger the message handler and return undefined if the message does not have a response", () => { - const message = { - command: "autofillOverlayElementClosed", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "overlayElementClosed"); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(null); - expect(sendResponse).not.toHaveBeenCalled(); - expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); - }); - - it("will return a response if the message handler returns a response", async () => { - const message = { - command: "openAutofillOverlay", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(true); - }); - - describe("extension message handlers", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockResolvedValue(AuthenticationStatus.Unlocked); - }); - - describe("openAutofillOverlay message handler", () => { - it("opens the autofill overlay by sending a message to the current tab", async () => { - const sender = mock({ tab: { id: 1 } }); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendMockExtensionMessage({ command: "openAutofillOverlay" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: false, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("autofillOverlayElementClosed message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { - const port1 = mock(); - const port2 = mock(); - overlayBackground["expiredPorts"] = [port1, port2]; - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage( - { - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }, - sender, - ); - - expect(port1.disconnect).toHaveBeenCalled(); - expect(port2.disconnect).toHaveBeenCalled(); - }); - - it("disconnects the button element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayButtonPort"]).toBeNull(); - }); - - it("disconnects the list element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayListPort"]).toBeNull(); - }); - }); - - describe("autofillOverlayAddNewVaultItem message handler", () => { - let sender: chrome.runtime.MessageSender; - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - jest - .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") - .mockImplementation(); - jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); - }); - - it("will not open the add edit popout window if the message does not have a login cipher provided", () => { - sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); - }); - - it("will open the add edit popout window after creating a new cipher", async () => { - jest.spyOn(BrowserApi, "sendMessage"); - - sendMockExtensionMessage( - { - command: "autofillOverlayAddNewVaultItem", - login: { - uri: "https://tacos.com", - hostname: "", - username: "username", - password: "password", - }, - }, - sender, - ); - await flushPromises(); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(BrowserApi.sendMessage).toHaveBeenCalledWith( - "inlineAutofillMenuRefreshAddEditCipher", - ); - expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); - }); - }); - - describe("getAutofillOverlayVisibility message handler", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - }); - - it("will set the overlayVisibility property", async () => { - sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); - await flushPromises(); - - expect(await overlayBackground["getOverlayVisibility"]()).toBe( - AutofillOverlayVisibility.OnFieldFocus, - ); - }); - - it("returns the overlayVisibility property", async () => { - const sendMessageSpy = jest.fn(); - - sendMockExtensionMessage( - { command: "getAutofillOverlayVisibility" }, - undefined, - sendMessageSpy, - ); - await flushPromises(); - - expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); - }); - }); - - describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("will check if the overlay list is focused if the list port is open", () => { - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - }); - - it("will check if the overlay button is focused if the list port is not open", () => { - overlayBackground["overlayListPort"] = undefined; - - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - }); - }); - - describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - - sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); - }); - }); - - describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(async () => { - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.List), - ); - listPortSpy = overlayBackground["overlayListPort"]; - - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.Button), - ); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the overlay button's position", () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the overlay button's height for medium sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the overlay button's height for large sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("will post a message to the overlay list facilitating an update of the list's position", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - overlayBackground["updateOverlayPosition"]( - { overlayElement: AutofillOverlayElement.List }, - sender, - ); - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - }); - - describe("updateOverlayHidden", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the display value is not provided", () => { - const message = { - command: "updateAutofillOverlayHidden", - }; - - sendMockExtensionMessage(message); - - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); - }); - - it("posts a message to the overlay button and list with the display value", () => { - const message = { command: "updateAutofillOverlayHidden", display: "none" }; - - sendMockExtensionMessage(message); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - }); - }); - - describe("collectPageDetailsResponse message handler", () => { - let sender: chrome.runtime.MessageSender; - const pageDetails1 = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - const pageDetails2 = createAutofillPageDetailsMock({ - login: { username: "username2", password: "password2" }, - }); - - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - }); - - it("stores the page details provided by the message by the tab id of the sender", () => { - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails1 }, - sender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]), - ); - }); - - it("updates the page details for a tab that already has a set of page details stored ", () => { - const secondFrameSender = mock({ - tab: { id: 1 }, - frameId: 3, - }); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]); - - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails2 }, - secondFrameSender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - [ - secondFrameSender.frameId, - { - frameId: secondFrameSender.frameId, - tab: secondFrameSender.tab, - details: pageDetails2, - }, - ], - ]), - ); - }); - }); - - describe("unlockCompleted message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; - - beforeEach(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(BrowserApi, "tabSendMessageData"); - getAuthStatusSpy = jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockImplementation(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - return Promise.resolve(AuthenticationStatus.Unlocked); - }); - }); - - it("updates the user's auth status but does not open the overlay", async () => { - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "" } }, - }, - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { - const sender = mock({ tab: { id: 1 } }); - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "openAutofillOverlay" } }, - }, - }; - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: true, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("extension messages that trigger an update of the inline menu ciphers", () => { - const extensionMessages = [ - "addedCipher", - "addEditCipherSubmitted", - "editedCipher", - "deletedCipher", - ]; - - beforeEach(() => { - jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); - }); - - extensionMessages.forEach((message) => { - it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { - sendMockExtensionMessage({ command: message }); - expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); - }); - }); - }); - }); - }); - - describe("handlePortOnConnect", () => { - beforeEach(() => { - jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); - jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); - }); - - it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { - const port = createPortSpyMock("not-an-overlay-element"); - - await overlayBackground["handlePortOnConnect"](port); - - expect(port.onMessage.addListener).not.toHaveBeenCalled(); - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("sets up the overlay list port if the port connection is for the overlay list", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - await flushPromises(); - - expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); - expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(listPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.List }, - listPortSpy.sender, - ); - }); - - it("sets up the overlay button port if the port connection is for the overlay button", async () => { - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["overlayListPort"]).toBeUndefined(); - expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(buttonPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.Button }, - buttonPortSpy.sender, - ); - }); - - it("stores an existing overlay port so that it can be disconnected at a later time", async () => { - overlayBackground["overlayButtonPort"] = mock(); - - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["expiredPorts"].length).toBe(1); - }); - - it("gets the system theme", async () => { - themeStateService.selectedTheme$ = of(ThemeType.System); - - await initOverlayElementPorts({ initList: true, initButton: false }); - await flushPromises(); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ theme: ThemeType.System }), - ); - }); - }); - - describe("handleOverlayElementPortMessage", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - }); - - it("ignores port messages that do not contain a handler", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); - }); - - describe("overlay button message handlers", () => { - it("unlocks the vault if the user auth status is not unlocked", () => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); - }); - - it("opens the autofill overlay if the auth status is unlocked", () => { - jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); - }); - - describe("closeAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: false }, - ); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks if the overlay list is focused", () => { - jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); - - sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - beforeEach(() => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - }); - - it("ignores the redirect message if the direction is not provided", () => { - sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); - - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("sends the redirect message if the direction is provided", () => { - sendPortMessage(buttonPortSpy, { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "redirectOverlayFocusOut", - { direction: RedirectFocusDirection.Next }, - ); - }); - }); - }); - - describe("overlay list message handlers", () => { - describe("checkAutofillOverlayButtonFocused", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("unlockVault", () => { - it("closes the autofill overlay and opens the unlock popout", async () => { - jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); - jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "unlockVault" }); - await flushPromises(); - - expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "addToLockedVaultPendingNotifications", - { - commandToRetry: { - message: { command: "openAutofillOverlay" }, - sender: listPortSpy.sender, - }, - target: "overlay.background", - }, - ); - expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - true, - ); - }); - }); - - describe("fillSelectedListItem", () => { - let getLoginCiphersSpy: jest.SpyInstance; - let isPasswordRepromptRequiredSpy: jest.SpyInstance; - let doAutoFillSpy: jest.SpyInstance; - let sender: chrome.runtime.MessageSender; - const pageDetails = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - - beforeEach(() => { - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy = jest.spyOn( - overlayBackground["autofillService"], - "isPasswordRepromptRequired", - ); - doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); - sender = mock({ tab: { id: 1 } }); - }); - - it("ignores the fill request if the overlay cipher id is not provided", async () => { - sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if the tab does not contain any identified page details", async () => { - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if a master password reprompt is required", async () => { - const cipher = mock({ - reprompt: CipherRepromptType.Password, - type: CipherType.Login, - }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy.mockResolvedValue(true); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - const cipher2 = mock({ id: "overlay-cipher-2" }); - const cipher3 = mock({ id: "overlay-cipher-3" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher2], - ["overlay-cipher-3", cipher3], - ]); - const pageDetailsForTab = { - frameId: sender.frameId, - tab: sender.tab, - details: pageDetails, - }; - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, pageDetailsForTab], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher2, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).toHaveBeenCalledWith({ - tab: listPortSpy.sender.tab, - cipher: cipher2, - pageDetails: [pageDetailsForTab], - fillNewPassword: true, - allowTotpAutofill: true, - }); - expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( - new Map([ - ["overlay-cipher-2", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-3", cipher3], - ]).entries(), - ); - }); - - it("copies the cipher's totp code to the clipboard after filling", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") - .mockImplementation(); - doAutoFillSpy.mockReturnValueOnce("totp-code"); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); - }); - }); - - describe("getNewVaultItemDetails", () => { - it("will send an addNewVaultItemFromOverlay message", async () => { - jest.spyOn(BrowserApi, "tabSendMessage"); - - sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { - command: "addNewVaultItemFromOverlay", - }); - }); - }); - - describe("viewSelectedCipher", () => { - let openViewVaultItemPopoutSpy: jest.SpyInstance; - - beforeEach(() => { - openViewVaultItemPopoutSpy = jest - .spyOn(overlayBackground as any, "openViewVaultItemPopout") - .mockImplementation(); - }); - - it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); - }); - - it("will open the view vault item popout with the selected cipher", async () => { - const cipher = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ["overlay-cipher-1", cipher], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }, - ); - }); - }); - - describe("redirectOverlayFocusOut", () => { - it("redirects focus out of the overlay list", async () => { - const message = { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }; - const redirectOverlayFocusOutSpy = jest.spyOn( - overlayBackground as any, - "redirectOverlayFocusOut", - ); - - sendPortMessage(listPortSpy, message); - await flushPromises(); - - expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); - }); - }); - }); - }); -}); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts deleted file mode 100644 index d0fad4cd00e..00000000000 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts +++ /dev/null @@ -1,812 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { 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 { SHOW_AUTOFILL_BUTTON } 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 } from "@bitwarden/common/autofill/types"; -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 { Utils } from "@bitwarden/common/platform/misc/utils"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; - -import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window"; -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { - openViewVaultItemPopout, - openAddEditVaultItemPopout, -} from "../../../vault/popup/utils/vault-popout-window"; -import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background"; -import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum"; -import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service"; - -import { - FocusedFieldData, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayCipherData, - OverlayListPortMessageHandlers, - OverlayBackgroundExtensionMessage, - OverlayAddNewItemMessage, - OverlayPortMessage, - WebsiteIconData, -} from "./abstractions/overlay.background.deprecated"; - -class LegacyOverlayBackground implements OverlayBackgroundInterface { - private readonly openUnlockPopout = openUnlockPopout; - private readonly openViewVaultItemPopout = openViewVaultItemPopout; - private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayLoginCiphers: Map = new Map(); - private pageDetailsForTab: Record< - chrome.runtime.MessageSender["tab"]["id"], - Map - > = {}; - private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; - private overlayButtonPort: chrome.runtime.Port; - private overlayListPort: chrome.runtime.Port; - private expiredPorts: chrome.runtime.Port[] = []; - private focusedFieldData: FocusedFieldData; - private overlayPageTranslations: Record; - private iconsServerUrl: string; - private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { - openAutofillOverlay: () => this.openOverlay(false), - autofillOverlayElementClosed: ({ message, sender }) => - this.overlayElementClosed(message, sender), - autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), - getAutofillOverlayVisibility: () => this.getOverlayVisibility(), - checkAutofillOverlayFocused: () => this.checkOverlayFocused(), - focusAutofillOverlayList: () => this.focusOverlayList(), - updateAutofillOverlayPosition: ({ message, sender }) => - this.updateOverlayPosition(message, sender), - updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), - updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), - collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), - unlockCompleted: ({ message }) => this.unlockCompleted(message), - addedCipher: () => this.updateOverlayCiphers(), - addEditCipherSubmitted: () => this.updateOverlayCiphers(), - editedCipher: () => this.updateOverlayCiphers(), - deletedCipher: () => this.updateOverlayCiphers(), - }; - private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { - overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), - closeAutofillOverlay: ({ port }) => this.closeOverlay(port), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayListFocused(), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), - }; - private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { - checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayButtonFocused(), - unlockVault: ({ port }) => this.unlockVault(port), - fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), - addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), - viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), - }; - - constructor( - private cipherService: CipherService, - private autofillService: AutofillService, - private authService: AuthService, - private environmentService: EnvironmentService, - private domainSettingsService: DomainSettingsService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private themeStateService: ThemeStateService, - private accountService: AccountService, - ) {} - - /** - * Removes cached page details for a tab - * based on the passed tabId. - * - * @param tabId - Used to reference the page details of a specific tab - */ - removePageDetails(tabId: number) { - if (!this.pageDetailsForTab[tabId]) { - return; - } - - this.pageDetailsForTab[tabId].clear(); - delete this.pageDetailsForTab[tabId]; - } - - /** - * Sets up the extension message listeners and gets the settings for the - * overlay's visibility and the user's authentication status. - */ - async init() { - this.setupExtensionMessageListeners(); - const env = await firstValueFrom(this.environmentService.environment$); - this.iconsServerUrl = env.getIconsUrl(); - await this.getOverlayVisibility(); - await this.getAuthStatus(); - } - - /** - * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. - * Queries all ciphers for the given url, and sorts them by last used. Will not update the - * list of ciphers if the extension is not unlocked. - */ - async updateOverlayCiphers() { - const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); - if (authStatus !== AuthenticationStatus.Unlocked) { - return; - } - - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab?.url) { - return; - } - - this.overlayLoginCiphers = new Map(); - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const ciphersViews = ( - await this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId) - ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); - for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { - this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); - } - - const ciphers = await this.getOverlayCipherData(); - this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); - await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { - isOverlayCiphersPopulated: Boolean(ciphers.length), - }); - } - - /** - * Strips out unnecessary data from the ciphers and returns an array of - * objects that contain the cipher data needed for the overlay list. - */ - private async getOverlayCipherData(): Promise { - const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); - const overlayCiphersArray = Array.from(this.overlayLoginCiphers); - const overlayCipherData: OverlayCipherData[] = []; - let loginCipherIcon: WebsiteIconData; - - for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { - const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; - if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); - } - - overlayCipherData.push({ - id: overlayCipherId, - name: cipher.name, - type: cipher.type, - reprompt: cipher.reprompt, - favorite: cipher.favorite, - icon: - cipher.type === CipherType.Login - ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), - login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, - card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, - }); - } - - return overlayCipherData; - } - - /** - * Handles aggregation of page details for a tab. Stores the page details - * in association with the tabId of the tab that sent the message. - * - * @param message - Message received from the `collectPageDetailsResponse` command - * @param sender - The sender of the message - */ - private storePageDetails( - message: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - const pageDetails = { - frameId: sender.frameId, - tab: sender.tab, - details: message.details, - }; - - const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; - if (!pageDetailsMap) { - this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); - return; - } - - pageDetailsMap.set(sender.frameId, pageDetails); - } - - /** - * Triggers autofill for the selected cipher in the overlay list. Also places - * the selected cipher at the top of the list of ciphers. - * - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message - */ - private async fillSelectedOverlayListItem( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!overlayCipherId || !pageDetails?.size) { - return; - } - - const cipher = this.overlayLoginCiphers.get(overlayCipherId); - - if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { - return; - } - const totpCode = await this.autofillService.doAutoFill({ - tab: sender.tab, - cipher: cipher, - pageDetails: Array.from(pageDetails.values()), - fillNewPassword: true, - allowTotpAutofill: true, - }); - - if (totpCode) { - this.platformUtilsService.copyToClipboard(totpCode); - } - - this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); - } - - /** - * Checks if the overlay is focused. Will check the overlay list - * if it is open, otherwise it will check the overlay button. - */ - private checkOverlayFocused() { - if (this.overlayListPort) { - this.checkOverlayListFocused(); - - return; - } - - this.checkOverlayButtonFocused(); - } - - /** - * Posts a message to the overlay button iframe to check if it is focused. - */ - private checkOverlayButtonFocused() { - this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); - } - - /** - * Posts a message to the overlay list iframe to check if it is focused. - */ - private checkOverlayListFocused() { - this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); - } - - /** - * Sends a message to the sender tab to close the autofill overlay. - * - * @param sender - The sender of the port message - * @param forceCloseOverlay - Identifies whether the overlay should be force closed - */ - private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = 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 - BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); - } - - /** - * Handles cleanup when an overlay element is closed. Disconnects - * the list and button ports and sets them to null. - * - * @param overlayElement - The overlay element that was closed, either the list or button - * @param sender - The sender of the port message - */ - private overlayElementClosed( - { overlayElement }: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { - this.expiredPorts.forEach((port) => port.disconnect()); - this.expiredPorts = []; - return; - } - - if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.disconnect(); - this.overlayButtonPort = null; - - return; - } - - this.overlayListPort?.disconnect(); - this.overlayListPort = null; - } - - /** - * Updates the position of either the overlay list or button. The position - * is based on the focused field's position and dimensions. - * - * @param overlayElement - The overlay element to update, either the list or button - * @param sender - The sender of the port message - */ - private updateOverlayPosition( - { overlayElement }: { overlayElement?: string }, - sender: chrome.runtime.MessageSender, - ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { - return; - } - - if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayButtonPosition(), - }); - - return; - } - - this.overlayListPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayListPosition(), - }); - } - - /** - * Gets the position of the focused field and calculates the position - * of the overlay button based on the focused field's position and dimensions. - */ - private getOverlayButtonPosition() { - if (!this.focusedFieldData) { - return; - } - - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; - const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; - let elementOffset = height * 0.37; - if (height >= 35) { - elementOffset = height >= 50 ? height * 0.47 : height * 0.42; - } - - const elementHeight = height - elementOffset; - const elementTopPosition = top + elementOffset / 2; - let elementLeftPosition = left + width - height + elementOffset / 2; - - const fieldPaddingRight = parseInt(paddingRight, 10); - const fieldPaddingLeft = parseInt(paddingLeft, 10); - if (fieldPaddingRight > fieldPaddingLeft) { - elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); - } - - return { - top: `${Math.round(elementTopPosition)}px`, - left: `${Math.round(elementLeftPosition)}px`, - height: `${Math.round(elementHeight)}px`, - width: `${Math.round(elementHeight)}px`, - }; - } - - /** - * Gets the position of the focused field and calculates the position - * of the overlay list based on the focused field's position and dimensions. - */ - private getOverlayListPosition() { - if (!this.focusedFieldData) { - return; - } - - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; - return { - width: `${Math.round(width)}px`, - top: `${Math.round(top + height)}px`, - left: `${Math.round(left)}px`, - }; - } - - /** - * Sets the focused field data to the data passed in the extension message. - * - * @param focusedFieldData - Contains the rects and styles of the focused field. - * @param sender - The sender of the extension message - */ - private setFocusedFieldData( - { focusedFieldData }: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; - } - - /** - * Updates the overlay's visibility based on the display property passed in the extension message. - * - * @param display - The display property of the overlay, either "block" or "none" - */ - private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { - if (!display) { - return; - } - - const portMessage = { command: "updateOverlayHidden", styles: { display } }; - - this.overlayButtonPort?.postMessage(portMessage); - this.overlayListPort?.postMessage(portMessage); - } - - /** - * Sends a message to the currently active tab to open the autofill overlay. - * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened - * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states - */ - private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - - await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { - isFocusingFieldElement, - isOpeningFullOverlay, - authStatus: await this.getAuthStatus(), - }); - } - - /** - * Gets the overlay's visibility setting from the settings service. - */ - private async getOverlayVisibility(): Promise { - return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); - } - - /** - * Gets the user's authentication status from the auth service. If the user's - * authentication status has changed, the overlay button's authentication status - * will be updated and the overlay list's ciphers will be updated. - */ - private async getAuthStatus() { - const formerAuthStatus = this.userAuthStatus; - this.userAuthStatus = await this.authService.getAuthStatus(); - - if ( - this.userAuthStatus !== formerAuthStatus && - this.userAuthStatus === AuthenticationStatus.Unlocked - ) { - this.updateOverlayButtonAuthStatus(); - await this.updateOverlayCiphers(); - } - - return this.userAuthStatus; - } - - /** - * Sends a message to the overlay button to update its authentication status. - */ - private updateOverlayButtonAuthStatus() { - this.overlayButtonPort?.postMessage({ - command: "updateOverlayButtonAuthStatus", - authStatus: this.userAuthStatus, - }); - } - - /** - * Handles the overlay button being clicked. If the user is not authenticated, - * the vault will be unlocked. If the user is authenticated, the overlay will - * be opened. - * - * @param port - The port of the overlay button - */ - private handleOverlayButtonClicked(port: chrome.runtime.Port) { - if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { - // 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.unlockVault(port); - 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.openOverlay(false, true); - } - - /** - * Facilitates opening the unlock popout window. - * - * @param port - The port of the overlay list - */ - private async unlockVault(port: chrome.runtime.Port) { - const { sender } = port; - - this.closeOverlay(port); - const retryMessage: LockedVaultPendingNotificationsData = { - commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, - target: "overlay.background", - }; - await BrowserApi.tabSendMessageData( - sender.tab, - "addToLockedVaultPendingNotifications", - retryMessage, - ); - await this.openUnlockPopout(sender.tab, true); - } - - /** - * Triggers the opening of a vault item popout window associated - * with the passed cipher ID. - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message - */ - private async viewSelectedCipher( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - const cipher = this.overlayLoginCiphers.get(overlayCipherId); - if (!cipher) { - return; - } - - await this.openViewVaultItemPopout(sender.tab, { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }); - } - - /** - * Facilitates redirecting focus to the overlay list. - */ - private focusOverlayList() { - this.overlayListPort?.postMessage({ command: "focusOverlayList" }); - } - - /** - * Updates the authentication status for the user and opens the overlay if - * a followup command is present in the message. - * - * @param message - Extension message received from the `unlockCompleted` command - */ - private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { - await this.getAuthStatus(); - - if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { - await this.openOverlay(true); - } - } - - /** - * Gets the translations for the overlay page. - */ - private getTranslations() { - if (!this.overlayPageTranslations) { - this.overlayPageTranslations = { - locale: BrowserApi.getUILanguage(), - opensInANewWindow: this.i18nService.translate("opensInANewWindow"), - buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), - toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), - listPageTitle: this.i18nService.translate("bitwardenVault"), - unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), - unlockAccount: this.i18nService.translate("unlockAccount"), - fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - partialUsername: this.i18nService.translate("partialUsername"), - view: this.i18nService.translate("view"), - noItemsToShow: this.i18nService.translate("noItemsToShow"), - newItem: this.i18nService.translate("newItem"), - addNewVaultItem: this.i18nService.translate("addNewVaultItem"), - }; - } - - return this.overlayPageTranslations; - } - - /** - * Facilitates redirecting focus out of one of the - * overlay elements to elements on the page. - * - * @param direction - The direction to redirect focus to (either "next", "previous" or "current) - * @param sender - The sender of the port message - */ - private redirectOverlayFocusOut( - { direction }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - if (!direction) { - 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 - BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); - } - - /** - * Triggers adding a new vault item from the overlay. Gathers data - * input by the user before calling to open the add/edit window. - * - * @param sender - The sender of the port message - */ - private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); - } - - /** - * Handles adding a new vault item from the overlay. Gathers data login - * data captured in the extension message. - * - * @param login - The login data captured from the extension message - * @param sender - The sender of the extension message - */ - private async addNewVaultItem( - { login }: OverlayAddNewItemMessage, - sender: chrome.runtime.MessageSender, - ) { - if (!login) { - return; - } - - const uriView = new LoginUriView(); - uriView.uri = login.uri; - - const loginView = new LoginView(); - loginView.uris = [uriView]; - loginView.username = login.username || ""; - loginView.password = login.password || ""; - - const cipherView = new CipherView(); - cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, ""); - cipherView.folderId = null; - cipherView.type = CipherType.Login; - cipherView.login = loginView; - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - await this.cipherService.setAddEditCipherInfo( - { - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }, - activeUserId, - ); - - await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); - } - - /** - * Sets up the extension message listeners for the overlay. - */ - private setupExtensionMessageListeners() { - BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); - BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); - } - - /** - * Handles extension messages sent to the extension background. - * - * @param message - The message received from the extension - * @param sender - The sender of the message - * @param sendResponse - The response to send back to the sender - */ - private handleExtensionMessage = ( - message: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - sendResponse: (response?: any) => void, - ) => { - const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; - if (!handler) { - return null; - } - - const messageResponse = handler({ message, sender }); - if (typeof messageResponse === "undefined") { - return null; - } - - // 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; - }; - - /** - * Handles the connection of a port to the extension background. - * - * @param port - The port that connected to the extension background - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isOverlayListPort = port.name === AutofillOverlayPort.List; - const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; - if (!isOverlayListPort && !isOverlayButtonPort) { - return; - } - - this.storeOverlayPort(port); - port.onMessage.addListener(this.handleOverlayElementPortMessage); - port.postMessage({ - command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, - authStatus: await this.getAuthStatus(), - styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), - theme: await firstValueFrom(this.themeStateService.selectedTheme$), - translations: this.getTranslations(), - ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, - }); - this.updateOverlayPosition( - { - overlayElement: isOverlayListPort - ? AutofillOverlayElement.List - : AutofillOverlayElement.Button, - }, - port.sender, - ); - }; - - /** - * Stores the connected overlay port and sets up any existing ports to be disconnected. - * - * @param port - The port to store -| */ - private storeOverlayPort(port: chrome.runtime.Port) { - if (port.name === AutofillOverlayPort.List) { - this.storeExpiredOverlayPort(this.overlayListPort); - this.overlayListPort = port; - return; - } - - if (port.name === AutofillOverlayPort.Button) { - this.storeExpiredOverlayPort(this.overlayButtonPort); - this.overlayButtonPort = port; - } - } - - /** - * When registering a new connection, we want to ensure that the port is disconnected. - * This method places an existing port in the expiredPorts array to be disconnected - * at a later time. - * - * @param port - The port to store in the expiredPorts array - */ - private storeExpiredOverlayPort(port: chrome.runtime.Port | null) { - if (port) { - this.expiredPorts.push(port); - } - } - - /** - * Handles messages sent to the overlay list or button ports. - * - * @param message - The message received from the port - * @param port - The port that sent the message - */ - private handleOverlayElementPortMessage = ( - message: OverlayBackgroundExtensionMessage, - port: chrome.runtime.Port, - ) => { - const command = message?.command; - let handler: CallableFunction | undefined; - - if (port.name === AutofillOverlayPort.Button) { - handler = this.overlayButtonPortMessageHandlers[command]; - } - - if (port.name === AutofillOverlayPort.List) { - handler = this.overlayListPortMessageHandlers[command]; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -export default LegacyOverlayBackground; diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts deleted file mode 100644 index ed422822b36..00000000000 --- a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -import AutofillScript from "../../../models/autofill-script"; - -type AutofillExtensionMessage = { - command: string; - tab?: chrome.tabs.Tab; - sender?: string; - fillScript?: AutofillScript; - url?: string; - pageDetailsUrl?: string; - ciphers?: any; - data?: { - authStatus?: AuthenticationStatus; - isFocusingFieldElement?: boolean; - isOverlayCiphersPopulated?: boolean; - direction?: "previous" | "next"; - isOpeningFullOverlay?: boolean; - forceCloseOverlay?: boolean; - autofillOverlayVisibility?: number; - }; -}; - -type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; - -type AutofillExtensionMessageHandlers = { - [key: string]: CallableFunction; - collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; - collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; - fillForm: ({ message }: AutofillExtensionMessageParam) => void; - openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - addNewVaultItemFromOverlay: () => void; - redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; - updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; - bgUnlockPopoutOpened: () => void; - bgVaultItemRepromptPopoutOpened: () => void; - updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; -}; - -export { AutofillExtensionMessage, AutofillExtensionMessageHandlers }; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts deleted file mode 100644 index 96d5e85ca34..00000000000 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; - -import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum"; -import AutofillPageDetails from "../../models/autofill-page-details"; -import AutofillScript from "../../models/autofill-script"; -import { - flushPromises, - mockQuerySelectorAllDefinedCall, - sendMockExtensionMessage, -} from "../../spec/testing-utils"; -import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated"; - -import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated"; -import AutofillInitDeprecated from "./autofill-init.deprecated"; - -describe("AutofillInit", () => { - let autofillInit: AutofillInitDeprecated; - const autofillOverlayContentService = mock(); - const originalDocumentReadyState = document.readyState; - const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); - - beforeEach(() => { - chrome.runtime.connect = jest.fn().mockReturnValue({ - onDisconnect: { - addListener: jest.fn(), - }, - }); - autofillInit = new AutofillInitDeprecated(autofillOverlayContentService); - window.IntersectionObserver = jest.fn(() => mock()); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - Object.defineProperty(document, "readyState", { - value: originalDocumentReadyState, - writable: true, - }); - }); - - afterAll(() => { - mockQuerySelectorAll.mockRestore(); - }); - - describe("init", () => { - it("sets up the extension message listeners", () => { - jest.spyOn(autofillInit as any, "setupExtensionMessageListeners"); - - autofillInit.init(); - - expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled(); - }); - - it("triggers a collection of page details if the document is in a `complete` ready state", () => { - jest.useFakeTimers(); - Object.defineProperty(document, "readyState", { value: "complete", writable: true }); - - autofillInit.init(); - jest.advanceTimersByTime(250); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( - { - command: "bgCollectPageDetails", - sender: "autofillInit", - }, - expect.any(Function), - ); - }); - - it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { - jest.spyOn(window, "addEventListener"); - Object.defineProperty(document, "readyState", { value: "loading", writable: true }); - - autofillInit.init(); - - expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); - }); - }); - - describe("setupExtensionMessageListeners", () => { - it("sets up a chrome runtime on message listener", () => { - jest.spyOn(chrome.runtime.onMessage, "addListener"); - - autofillInit["setupExtensionMessageListeners"](); - - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( - autofillInit["handleExtensionMessage"], - ); - }); - }); - - describe("handleExtensionMessage", () => { - let message: AutofillExtensionMessage; - let sender: chrome.runtime.MessageSender; - const sendResponse = jest.fn(); - - beforeEach(() => { - message = { - command: "collectPageDetails", - tab: mock(), - sender: "sender", - }; - sender = mock(); - }); - - it("returns a undefined value if a extension message handler is not found with the given message command", () => { - message.command = "unknownCommand"; - - const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - - expect(response).toBe(null); - }); - - it("returns a undefined value if the message handler does not return a response", async () => { - const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response1).not.toBe(false); - - message.command = "removeAutofillOverlay"; - message.fillScript = mock(); - - const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response2).toBe(null); - }); - - it("returns a true value and calls sendResponse if the message handler returns a response", async () => { - message.command = "collectPageDetailsImmediately"; - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response).toBe(true); - expect(sendResponse).toHaveBeenCalledWith(pageDetails); - }); - - describe("extension message handlers", () => { - beforeEach(() => { - autofillInit.init(); - }); - - describe("collectPageDetails", () => { - it("sends the collected page details for autofill using a background script message", async () => { - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - const message = { - command: "collectPageDetails", - sender: "sender", - tab: mock(), - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - sendMockExtensionMessage(message, sender, sendResponse); - await flushPromises(); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - }); - }); - - describe("collectPageDetailsImmediately", () => { - it("returns collected page details for autofill if set to send the details in the response", async () => { - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - sendMockExtensionMessage( - { command: "collectPageDetailsImmediately" }, - sender, - sendResponse, - ); - await flushPromises(); - - expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled(); - expect(sendResponse).toBeCalledWith(pageDetails); - expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - }); - }); - - describe("fillForm", () => { - let fillScript: AutofillScript; - beforeEach(() => { - fillScript = mock(); - jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation(); - }); - - it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { - const fillScript = mock(); - const message = { - command: "fillForm", - fillScript, - pageDetailsUrl: "https://a-different-url.com", - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( - fillScript, - ); - }); - - it("calls the InsertAutofillContentService to fill the form", async () => { - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - - expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - }); - - it("removes the overlay when filling the form", async () => { - const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - - expect(blurAndRemoveOverlaySpy).toHaveBeenCalled(); - }); - - it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { - jest.useFakeTimers(); - jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); - expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); - }); - - it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { - jest.useFakeTimers(); - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( - 1, - true, - ); - expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( - 2, - false, - ); - }); - }); - - describe("openAutofillOverlay", () => { - const message = { - command: "openAutofillOverlay", - data: { - isFocusingFieldElement: true, - isOpeningFullOverlay: true, - authStatus: AuthenticationStatus.Unlocked, - }, - }; - - it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("opens the autofill overlay", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].openAutofillOverlay, - ).toHaveBeenCalledWith({ - isFocusingFieldElement: message.data.isFocusingFieldElement, - isOpeningFullOverlay: message.data.isOpeningFullOverlay, - authStatus: message.data.authStatus, - }); - }); - }); - - describe("closeAutofillOverlay", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; - }); - - it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: false }, - }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("removes the autofill overlay if the message flags a forced closure", () => { - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: true }, - }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - - it("ignores the message if a field is currently focused", () => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the autofill overlay list if the overlay is currently filling", () => { - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the entire overlay if the overlay is not currently filling", () => { - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - }); - - describe("addNewVaultItemFromOverlay", () => { - it("will not add a new vault item if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("will add a new vault item", () => { - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - const message = { - command: "redirectOverlayFocusOut", - data: { - direction: RedirectFocusDirection.Next, - }, - }; - - it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("redirects the overlay focus", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, - ).toHaveBeenCalledWith(message.data.direction); - }); - }); - - describe("updateIsOverlayCiphersPopulated", () => { - const message = { - command: "updateIsOverlayCiphersPopulated", - data: { - isOverlayCiphersPopulated: true, - }, - }; - - it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("updates whether the overlay ciphers are populated", () => { - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( - message.data.isOverlayCiphersPopulated, - ); - }); - }); - - describe("bgUnlockPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("bgVaultItemRepromptPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("updateAutofillOverlayVisibility", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = - AutofillOverlayVisibility.OnButtonClick; - }); - - it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { - sendMockExtensionMessage({ - command: "updateAutofillOverlayVisibility", - data: {}, - }); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - AutofillOverlayVisibility.OnButtonClick, - ); - }); - - it("updates the overlay visibility value", () => { - const message = { - command: "updateAutofillOverlayVisibility", - data: { - autofillOverlayVisibility: AutofillOverlayVisibility.Off, - }, - }; - - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - message.data.autofillOverlayVisibility, - ); - }); - }); - }); - }); - - describe("destroy", () => { - it("clears the timeout used to collect page details on load", () => { - jest.spyOn(window, "clearTimeout"); - - autofillInit.init(); - autofillInit.destroy(); - - expect(window.clearTimeout).toHaveBeenCalledWith( - autofillInit["collectPageDetailsOnLoadTimeout"], - ); - }); - - it("removes the extension message listeners", () => { - autofillInit.destroy(); - - expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith( - autofillInit["handleExtensionMessage"], - ); - }); - - it("destroys the collectAutofillContentService", () => { - jest.spyOn(autofillInit["collectAutofillContentService"], "destroy"); - - autofillInit.destroy(); - - expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts deleted file mode 100644 index fac9c0852f5..00000000000 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts +++ /dev/null @@ -1,315 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { AutofillInit } from "../../content/abstractions/autofill-init"; -import AutofillPageDetails from "../../models/autofill-page-details"; -import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; -import DomElementVisibilityService from "../../services/dom-element-visibility.service"; -import { DomQueryService } from "../../services/dom-query.service"; -import InsertAutofillContentService from "../../services/insert-autofill-content.service"; -import { sendExtensionMessage } from "../../utils"; -import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; - -import { - AutofillExtensionMessage, - AutofillExtensionMessageHandlers, -} from "./abstractions/autofill-init.deprecated"; - -class LegacyAutofillInit implements AutofillInit { - private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined; - private readonly domElementVisibilityService: DomElementVisibilityService; - private readonly collectAutofillContentService: CollectAutofillContentService; - private readonly insertAutofillContentService: InsertAutofillContentService; - private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; - private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { - collectPageDetails: ({ message }) => this.collectPageDetails(message), - collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), - fillForm: ({ message }) => this.fillForm(message), - openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), - closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), - addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), - redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), - updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), - bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), - bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), - updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), - }; - - /** - * AutofillInit constructor. Initializes the DomElementVisibilityService, - * CollectAutofillContentService and InsertAutofillContentService classes. - * - * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. - */ - constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { - this.autofillOverlayContentService = autofillOverlayContentService; - this.domElementVisibilityService = new DomElementVisibilityService(); - const domQueryService = new DomQueryService(); - this.collectAutofillContentService = new CollectAutofillContentService( - this.domElementVisibilityService, - domQueryService, - this.autofillOverlayContentService, - ); - this.insertAutofillContentService = new InsertAutofillContentService( - this.domElementVisibilityService, - this.collectAutofillContentService, - ); - } - - /** - * Initializes the autofill content script, setting up - * the extension message listeners. This method should - * be called once when the content script is loaded. - */ - init() { - this.setupExtensionMessageListeners(); - this.autofillOverlayContentService?.init(); - this.collectPageDetailsOnLoad(); - } - - /** - * Triggers a collection of the page details from the - * background script, ensuring that autofill is ready - * to act on the page. - */ - private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => { - this.clearCollectPageDetailsOnLoadTimeout(); - this.collectPageDetailsOnLoadTimeout = setTimeout( - () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), - 250, - ); - }; - - if (globalThis.document.readyState === "complete") { - sendCollectDetailsMessage(); - } - - globalThis.addEventListener("load", sendCollectDetailsMessage); - } - - /** - * Collects the page details and sends them to the - * extension background script. If the `sendDetailsInResponse` - * parameter is set to true, the page details will be - * returned to facilitate sending the details in the - * response to the extension message. - * - * @param message - The extension message. - * @param sendDetailsInResponse - Determines whether to send the details in the response. - */ - private async collectPageDetails( - message: AutofillExtensionMessage, - sendDetailsInResponse = false, - ): Promise { - const pageDetails: AutofillPageDetails = - await this.collectAutofillContentService.getPageDetails(); - if (sendDetailsInResponse) { - return pageDetails; - } - - void chrome.runtime.sendMessage({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - } - - /** - * Fills the form with the given fill script. - * - * @param {AutofillExtensionMessage} message - */ - private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { - if ((document.defaultView || window).location.href !== pageDetailsUrl) { - return; - } - - this.blurAndRemoveOverlay(); - this.updateOverlayIsCurrentlyFilling(true); - await this.insertAutofillContentService.fillForm(fillScript); - - if (!this.autofillOverlayContentService) { - return; - } - - setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); - } - - /** - * Handles updating the overlay is currently filling value. - * - * @param isCurrentlyFilling - Indicates if the overlay is currently filling - */ - private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; - } - - /** - * Opens the autofill overlay. - * - * @param data - The extension message data. - */ - private openAutofillOverlay({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.openAutofillOverlay(data); - } - - /** - * Blurs the most recent overlay field and removes the overlay. Used - * in cases where the background unlock or vault item reprompt popout - * is opened. - */ - private blurAndRemoveOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - } - - /** - * Removes the autofill overlay if the field is not currently focused. - * If the autofill is currently filling, only the overlay list will be - * removed. - */ - private removeAutofillOverlay(message?: AutofillExtensionMessage) { - if (message?.data?.forceCloseOverlay) { - this.autofillOverlayContentService?.removeAutofillOverlay(); - return; - } - - if ( - !this.autofillOverlayContentService || - this.autofillOverlayContentService.isFieldCurrentlyFocused - ) { - return; - } - - if (this.autofillOverlayContentService.isCurrentlyFilling) { - this.autofillOverlayContentService.removeAutofillOverlayList(); - return; - } - - this.autofillOverlayContentService.removeAutofillOverlay(); - } - - /** - * Adds a new vault item from the overlay. - */ - private addNewVaultItemFromOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.addNewVaultItem(); - } - - /** - * Redirects the overlay focus out of an overlay iframe. - * - * @param data - Contains the direction to redirect the focus. - */ - private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); - } - - /** - * Updates whether the current tab has ciphers that can populate the overlay list - * - * @param data - Contains the isOverlayCiphersPopulated value - * - */ - private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( - data?.isOverlayCiphersPopulated, - ); - } - - /** - * Updates the autofill overlay visibility. - * - * @param data - Contains the autoFillOverlayVisibility value - */ - private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { - return; - } - - this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; - } - - /** - * Clears the send collect details message timeout. - */ - private clearCollectPageDetailsOnLoadTimeout() { - if (this.collectPageDetailsOnLoadTimeout) { - clearTimeout(this.collectPageDetailsOnLoadTimeout); - } - } - - /** - * Sets up the extension message listeners for the content script. - */ - private setupExtensionMessageListeners() { - chrome.runtime.onMessage.addListener(this.handleExtensionMessage); - } - - /** - * Handles the extension messages sent to the content script. - * - * @param message - The extension message. - * @param sender - The message sender. - * @param sendResponse - The send response callback. - */ - private handleExtensionMessage = ( - message: AutofillExtensionMessage, - sender: chrome.runtime.MessageSender, - sendResponse: (response?: any) => void, - ): boolean => { - const command: string = message.command; - const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; - if (!handler) { - return null; - } - - const messageResponse = handler({ message, sender }); - if (typeof messageResponse === "undefined") { - return null; - } - - // 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; - }; - - /** - * Handles destroying the autofill init content script. Removes all - * listeners, timeouts, and object instances to prevent memory leaks. - */ - destroy() { - this.clearCollectPageDetailsOnLoadTimeout(); - chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); - this.collectAutofillContentService.destroy(); - this.autofillOverlayContentService?.destroy(); - } -} - -export default LegacyAutofillInit; diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts deleted file mode 100644 index 66d672172ae..00000000000 --- a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { setupAutofillInitDisconnectAction } from "../../utils"; -import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated"; - -import LegacyAutofillInit from "./autofill-init.deprecated"; - -(function (windowContext) { - if (!windowContext.bitwardenAutofillInit) { - const autofillOverlayContentService = new LegacyAutofillOverlayContentService(); - windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService); - setupAutofillInitDisconnectAction(windowContext); - - windowContext.bitwardenAutofillInit.init(); - } -})(window); diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts deleted file mode 100644 index b6b22be9439..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -type OverlayButtonMessage = { command: string; colorScheme?: string }; - -type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: AuthenticationStatus }; - -type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & { - styleSheetUrl: string; - translations: Record; -}; - -type OverlayButtonWindowMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayButton: ({ message }: { message: InitAutofillOverlayButtonMessage }) => void; - checkAutofillOverlayButtonFocused: () => void; - updateAutofillOverlayButtonAuthStatus: ({ - message, - }: { - message: UpdateAuthStatusMessage; - }) => void; - updateOverlayPageColorScheme: ({ message }: { message: OverlayButtonMessage }) => void; -}; - -export { - UpdateAuthStatusMessage, - OverlayButtonMessage, - InitAutofillOverlayButtonMessage, - OverlayButtonWindowMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts deleted file mode 100644 index 0c4160a0709..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts +++ /dev/null @@ -1,33 +0,0 @@ -type AutofillOverlayIframeExtensionMessage = { - command: string; - styles?: Partial; - theme?: string; -}; - -type AutofillOverlayIframeWindowMessageHandlers = { - [key: string]: CallableFunction; - updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void; - getPageColorScheme: () => void; -}; - -type AutofillOverlayIframeExtensionMessageParam = { - message: AutofillOverlayIframeExtensionMessage; -}; - -type BackgroundPortMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; - updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; - updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; -}; - -interface AutofillOverlayIframeService { - initOverlayIframe(initStyles: Partial, ariaAlert?: string): void; -} - -export { - AutofillOverlayIframeExtensionMessage, - AutofillOverlayIframeWindowMessageHandlers, - BackgroundPortMessageHandlers, - AutofillOverlayIframeService, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts deleted file mode 100644 index 83578b13043..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated"; - -type OverlayListMessage = { command: string }; - -type UpdateOverlayListCiphersMessage = OverlayListMessage & { - ciphers: OverlayCipherData[]; -}; - -type InitAutofillOverlayListMessage = OverlayListMessage & { - authStatus: AuthenticationStatus; - styleSheetUrl: string; - theme: string; - translations: Record; - ciphers?: OverlayCipherData[]; -}; - -type OverlayListWindowMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayList: ({ message }: { message: InitAutofillOverlayListMessage }) => void; - checkAutofillOverlayListFocused: () => void; - updateOverlayListCiphers: ({ message }: { message: UpdateOverlayListCiphersMessage }) => void; - focusOverlayList: () => void; -}; - -export { - UpdateOverlayListCiphersMessage, - InitAutofillOverlayListMessage, - OverlayListWindowMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts deleted file mode 100644 index 368ae4e7303..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated"; -import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated"; - -type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers; - -type AutofillOverlayPageElementWindowMessage = { - [key: string]: any; - command: string; - overlayCipherId?: string; - height?: number; -}; - -export { WindowMessageHandlers, AutofillOverlayPageElementWindowMessage }; diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap deleted file mode 100644 index 132bd968899..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AutofillOverlayIframeService initOverlayIframe creates an aria alert element if the ariaAlert param is passed 1`] = ` -
- aria alert -
-`; - -exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's attributes 1`] = ` -