1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-12 13:39:14 +01:00

Merge branch 'main' into autofill/pm-6546-blurring-of-autofilled-elements-causes-problems-in-blur-event-listeners

This commit is contained in:
Cesar Gonzalez 2024-03-13 12:06:34 -05:00 committed by GitHub
commit 6058d1257c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
249 changed files with 5197 additions and 2542 deletions

View File

@ -18,7 +18,6 @@ apps/desktop/src/auth/scripts/duo.js
apps/web/config.js apps/web/config.js
apps/web/scripts/*.js apps/web/scripts/*.js
apps/web/src/theme.js
apps/web/tailwind.config.js apps/web/tailwind.config.js
apps/cli/config/config.js apps/cli/config/config.js

View File

@ -37,7 +37,6 @@
"base64-loader", "base64-loader",
"buffer", "buffer",
"bufferutil", "bufferutil",
"clean-webpack-plugin",
"copy-webpack-plugin", "copy-webpack-plugin",
"core-js", "core-js",
"css-loader", "css-loader",

View File

@ -127,7 +127,9 @@ jobs:
# Note, before updating the ubuntu version of the workflow, ensure the snap base image # 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. # 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` # The snap base for desktop is defined in `apps/desktop/electron-builder.json`
runs-on: ubuntu-22.04 # We are currently running on 20.04 until the Ubuntu 24.04 release is available, as moving
# to 22.04 now breaks users who are on 20.04 due to mismatched GLIBC versions.
runs-on: ubuntu-20.04
needs: setup needs: setup
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}

View File

@ -6,37 +6,11 @@ on:
tags: tags:
- desktop-v** - desktop-v**
defaults:
run:
shell: bash
jobs: jobs:
bump-version: bump-version:
name: Bump Desktop Version name: Bump Desktop Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Calculate bumped version
id: version
env:
RELEASE_TAG: ${{ github.ref }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
echo "Current Major: $CURR_MAJOR"
echo "Current Patch: $CURR_PATCH"
NEW_PATCH=$((CURR_PATCH+1))
echo "New patch: $NEW_PATCH"
NEW_VER=$CURR_MAJOR.$NEW_PATCH
echo "New Version: $NEW_VER"
echo "New Version: $NEW_VER" >> $GITHUB_STEP_SUMMARY
echo "new_version=$NEW_VER" >> $GITHUB_OUTPUT
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with: with:
@ -49,10 +23,13 @@ jobs:
keyvault: bitwarden-ci keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope" secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: "Bump version to ${{ steps.version.outputs.new_version }}" - name: Trigger Version Bump workflow
env: env:
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
run: | run: |
echo '{"cut_rc_branch": "false", "version_number": "${{ steps.version.outputs.new_version }}", echo '{"cut_rc_branch": "false", \
"bump_browser": "false", "bump_cli": "false", "bump_desktop": "true", "bump_web": "false"}' | \ "bump_browser": "false", \
"bump_cli": "false", \
"bump_desktop": "true", \
"bump_web": "false"}' | \
gh workflow run version-bump.yml --json --repo bitwarden/clients gh workflow run version-bump.yml --json --repo bitwarden/clients

View File

@ -1,6 +1,5 @@
--- ---
name: Version Bump name: Version Bump
run-name: Version Bump - v${{ inputs.version_number }}
on: on:
workflow_dispatch: workflow_dispatch:
@ -21,9 +20,10 @@ on:
description: "Bump Web?" description: "Bump Web?"
type: boolean type: boolean
default: false default: false
version_number: version_number_override:
description: "New version (example: '2024.1.0')" description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: true required: false
type: string
cut_rc_branch: cut_rc_branch:
description: "Cut RC branch?" description: "Cut RC branch?"
default: true default: true
@ -31,22 +31,19 @@ on:
jobs: jobs:
bump_version: bump_version:
name: "Bump Version to v${{ inputs.version_number }}" name: Bump Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
outputs:
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
version_web: ${{ steps.set-final-version-output.outputs.version_web }}
steps: steps:
- name: Login to Azure - Prod Subscription - name: Validate version input
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} version: ${{ inputs.version_number_override }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout Branch - name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@ -63,6 +60,20 @@ jobs:
exit 1 exit 1
fi fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Import GPG key - name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
with: with:
@ -71,6 +82,11 @@ jobs:
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: true git_commit_gpgsign: true
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create Version Branch - name: Create Version Branch
id: create-branch id: create-branch
run: | run: |
@ -90,7 +106,7 @@ jobs:
printf -v joined '%s,' "${CLIENTS[@]}" printf -v joined '%s,' "${CLIENTS[@]}"
echo "client=${joined%,}" >> $GITHUB_OUTPUT echo "client=${joined%,}" >> $GITHUB_OUTPUT
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }} NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT
@ -99,13 +115,20 @@ jobs:
######################## ########################
### Browser ### Browser
- name: Browser - Verify input version - name: Get current Browser version
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true }}
env: id: current-browser-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/browser
- name: Browser - Verify input version
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-browser-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -122,23 +145,52 @@ jobs:
fi fi
working-directory: apps/browser working-directory: apps/browser
- name: Bump Browser Version - name: Calculate next Browser release version
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number }} id: calculate-next-browser-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-browser-version.outputs.version }}
- name: Bump Browser Version - Manifest - name: Bump Browser Version - Version Override
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
id: bump-browser-version-override
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }}
- name: Bump Browser Version - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
id: bump-browser-version-automatic
env:
VERSION: ${{ steps.calculate-next-browser-version.outputs.version }}
run: npm version --workspace=@bitwarden/browser $VERSION
- name: Bump Browser Version - Manifest - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "apps/browser/src/manifest.json" file_path: "apps/browser/src/manifest.json"
version: ${{ inputs.version_number_override }}
- name: Bump Browser Version - Manifest v3 - name: Bump Browser Version - Manifest - Automatic Calculation
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "apps/browser/src/manifest.json"
version: ${{ steps.calculate-next-browser-version.outputs.version }}
- name: Bump Browser Version - Manifest v3 - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "apps/browser/src/manifest.v3.json" file_path: "apps/browser/src/manifest.v3.json"
version: ${{ inputs.version_number_override }}
- name: Bump Browser Version - Manifest v3 - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "apps/browser/src/manifest.v3.json"
version: ${{ steps.calculate-next-browser-version.outputs.version }}
- name: Run Prettier after Browser Version Bump - name: Run Prettier after Browser Version Bump
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true }}
@ -148,13 +200,20 @@ jobs:
prettier --write apps/browser/src/manifest.v3.json prettier --write apps/browser/src/manifest.v3.json
### CLI ### CLI
- name: CLI - Verify input version - name: Get current CLI version
if: ${{ inputs.bump_cli == true }} if: ${{ inputs.bump_cli == true }}
env: id: current-cli-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/cli
- name: CLI - Verify input version
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-cli-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -171,18 +230,40 @@ jobs:
fi fi
working-directory: apps/cli working-directory: apps/cli
- name: Bump CLI Version - name: Calculate next CLI release version
if: ${{ inputs.bump_cli == true }} if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number }} id: calculate-next-cli-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-cli-version.outputs.version }}
- name: Bump CLI Version - Version Override
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
id: bump-cli-version-override
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }}
- name: Bump CLI Version - Automatic Calculation
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
id: bump-cli-version-automatic
env:
VERSION: ${{ steps.calculate-next-cli-version.outputs.version }}
run: npm version --workspace=@bitwarden/cli $VERSION
### Desktop ### Desktop
- name: Desktop - Verify input version - name: Get current Desktop version
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true }}
env: id: current-desktop-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/desktop
- name: Desktop - Verify input version
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-desktop-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -199,23 +280,52 @@ jobs:
fi fi
working-directory: apps/desktop working-directory: apps/desktop
- name: Bump Desktop Version - Root - name: Calculate next Desktop release version
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number }} id: calculate-next-desktop-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-desktop-version.outputs.version }}
- name: Bump Desktop Version - App - name: Bump Desktop Version - Root - Version Override
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number }} id: bump-desktop-version-override
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }}
- name: Bump Desktop Version - Root - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
id: bump-desktop-version-automatic
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - App - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number_override }}
working-directory: "apps/desktop/src"
- name: Bump Desktop Version - App - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version $VERSION
working-directory: "apps/desktop/src" working-directory: "apps/desktop/src"
### Web ### Web
- name: Web - Verify input version - name: Get current Web version
if: ${{ inputs.bump_web == true }} if: ${{ inputs.bump_web == true }}
env: id: current-web-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/web
- name: Web - Verify input version
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-web-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -232,16 +342,47 @@ jobs:
fi fi
working-directory: apps/web working-directory: apps/web
- name: Bump Web Version - name: Calculate next Web release version
if: ${{ inputs.bump_web == true }} if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number }} id: calculate-next-web-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-web-version.outputs.version }}
- name: Bump Web Version - Version Override
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
id: bump-web-version-override
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }}
- name: Bump Web Version - Automatic Calculation
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
id: bump-web-version-automatic
env:
VERSION: ${{ steps.calculate-next-web-version.outputs.version }}
run: npm version --workspace=@bitwarden/web-vault $VERSION
######################## ########################
- name: Setup git - name: Set final version output
id: set-final-version-output
run: | run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then
git config --local user.name "bitwarden-devops-bot" echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed - name: Check if version changed
id: version-changed id: version-changed
@ -257,7 +398,7 @@ jobs:
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: env:
CLIENT: ${{ steps.create-branch.outputs.client }} CLIENT: ${{ steps.create-branch.outputs.client }}
VERSION: ${{ inputs.version_number }} VERSION: ${{ steps.set-final-version-output.outputs.version }}
run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
- name: Push changes - name: Push changes
@ -272,7 +413,7 @@ jobs:
env: env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }} PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ inputs.version_number }}" TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ steps.set-final-version-output.outputs.version }}"
run: | run: |
PR_URL=$(gh pr create --title "$TITLE" \ PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \ --base "main" \
@ -288,7 +429,7 @@ jobs:
- [X] Other - [X] Other
## Objective ## Objective
Automated ${{ steps.create-branch.outputs.client }} version bump to ${{ inputs.version_number }}") Automated ${{ steps.create-branch.outputs.client }} version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR - name: Approve PR
@ -307,8 +448,8 @@ jobs:
cut_rc: cut_rc:
name: Cut RC branch name: Cut RC branch
needs: bump_version
if: ${{ inputs.cut_rc_branch == true }} if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch - name: Checkout Branch
@ -320,7 +461,7 @@ jobs:
- name: Browser - Verify version has been updated - name: Browser - Verify version has been updated
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do
@ -338,7 +479,7 @@ jobs:
- name: CLI - Verify version has been updated - name: CLI - Verify version has been updated
if: ${{ inputs.bump_cli == true }} if: ${{ inputs.bump_cli == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do
@ -356,7 +497,7 @@ jobs:
- name: Desktop - Verify version has been updated - name: Desktop - Verify version has been updated
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do
@ -374,7 +515,7 @@ jobs:
- name: Web - Verify version has been updated - name: Web - Verify version has been updated
if: ${{ inputs.bump_web == true }} if: ${{ inputs.bump_web == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_web }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do

View File

@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2024.2.1", "version": "2024.3.0",
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack",

View File

@ -360,7 +360,7 @@
"message": "Gwefan" "message": "Gwefan"
}, },
"toggleVisibility": { "toggleVisibility": {
"message": "Toggle visibility" "message": "Toglo gwelededd"
}, },
"manage": { "manage": {
"message": "Rheoli" "message": "Rheoli"
@ -381,7 +381,7 @@
"message": "Ystyriwch ein helpu ni gydag adolygiad da!" "message": "Ystyriwch ein helpu ni gydag adolygiad da!"
}, },
"browserNotSupportClipboard": { "browserNotSupportClipboard": {
"message": "Your web browser does not support easy clipboard copying. Copy it manually instead." "message": "Dyw eich porwr gwe ddim yn cefnogi copïo drwy'r clipfwrdd yn hawdd. Copïwch â llaw yn lle."
}, },
"verifyIdentity": { "verifyIdentity": {
"message": "Gwirio'ch hunaniaeth" "message": "Gwirio'ch hunaniaeth"
@ -415,7 +415,7 @@
"message": "Cloi nawr" "message": "Cloi nawr"
}, },
"lockAll": { "lockAll": {
"message": "Lock all" "message": "Cloi'r cwbl"
}, },
"immediately": { "immediately": {
"message": "ar unwaith" "message": "ar unwaith"
@ -2925,7 +2925,7 @@
"message": "Newid i gyfrif" "message": "Newid i gyfrif"
}, },
"activeAccount": { "activeAccount": {
"message": "Active account" "message": "Cyfrif gweithredol"
}, },
"availableAccounts": { "availableAccounts": {
"message": "Cyfrifon ar gael" "message": "Cyfrifon ar gael"
@ -2943,7 +2943,7 @@
"message": "unlocked" "message": "unlocked"
}, },
"server": { "server": {
"message": "server" "message": "gweinydd"
}, },
"hostedAt": { "hostedAt": {
"message": "hosted at" "message": "hosted at"
@ -2952,7 +2952,7 @@
"message": "Use your device or hardware key" "message": "Use your device or hardware key"
}, },
"justOnce": { "justOnce": {
"message": "Just once" "message": "Unwaith yn unig"
}, },
"alwaysForThisSite": { "alwaysForThisSite": {
"message": "Always for this site" "message": "Always for this site"
@ -2971,7 +2971,7 @@
"description": "Label indicating the most common import formats" "description": "Label indicating the most common import formats"
}, },
"overrideDefaultBrowserAutofillTitle": { "overrideDefaultBrowserAutofillTitle": {
"message": "Make Bitwarden your default password manager?", "message": "Hoffech chi wneud Bitwarden yn rheolydd cyfrineiriau rhagosodedig?",
"description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior"
}, },
"overrideDefaultBrowserAutofillDescription": { "overrideDefaultBrowserAutofillDescription": {

View File

@ -1500,7 +1500,7 @@
"message": "无效 PIN 码。" "message": "无效 PIN 码。"
}, },
"tooManyInvalidPinEntryAttemptsLoggingOut": { "tooManyInvalidPinEntryAttemptsLoggingOut": {
"message": "无效的 PIN 输入尝试次数过多,正在注销。" "message": "无效的 PIN 输入尝试次数过多,正在退出登录。"
}, },
"unlockWithBiometrics": { "unlockWithBiometrics": {
"message": "使用生物识别解锁" "message": "使用生物识别解锁"

View File

@ -26,6 +26,10 @@ import {
factory, factory,
FactoryOptions, FactoryOptions,
} from "../../../platform/background/service-factories/factory-options"; } from "../../../platform/background/service-factories/factory-options";
import {
globalStateProviderFactory,
GlobalStateProviderInitOptions,
} from "../../../platform/background/service-factories/global-state-provider.factory";
import { import {
i18nServiceFactory, i18nServiceFactory,
I18nServiceInitOptions, I18nServiceInitOptions,
@ -84,7 +88,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
PolicyServiceInitOptions & PolicyServiceInitOptions &
PasswordStrengthServiceInitOptions & PasswordStrengthServiceInitOptions &
DeviceTrustCryptoServiceInitOptions & DeviceTrustCryptoServiceInitOptions &
AuthRequestServiceInitOptions; AuthRequestServiceInitOptions &
GlobalStateProviderInitOptions;
export function loginStrategyServiceFactory( export function loginStrategyServiceFactory(
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
@ -113,6 +118,7 @@ export function loginStrategyServiceFactory(
await policyServiceFactory(cache, opts), await policyServiceFactory(cache, opts),
await deviceTrustCryptoServiceFactory(cache, opts), await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
), ),
); );
} }

View File

@ -1,3 +1,4 @@
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
@ -111,6 +112,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>; collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>; bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>; bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
bgGetExcludedDomains: () => Promise<NeverDomains>;
getWebVaultUrlForNotification: () => string; getWebVaultUrlForNotification: () => string;
}; };

View File

@ -4,9 +4,11 @@ import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
@ -47,8 +49,10 @@ describe("NotificationBackground", () => {
const folderService = mock<FolderService>(); const folderService = mock<FolderService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>(); const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
const environmentService = mock<EnvironmentService>(); const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const themeStateService = mock<ThemeStateService>();
beforeEach(() => { beforeEach(() => {
notificationBackground = new NotificationBackground( notificationBackground = new NotificationBackground(
@ -59,8 +63,10 @@ describe("NotificationBackground", () => {
folderService, folderService,
stateService, stateService,
userNotificationSettingsService, userNotificationSettingsService,
domainSettingsService,
environmentService, environmentService,
logService, logService,
themeStateService,
); );
}); });

View File

@ -5,10 +5,13 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants"; import { NOTIFICATION_BAR_LIFESPAN_MS } 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 { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@ -60,6 +63,7 @@ export default class NotificationBackground {
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
bgGetExcludedDomains: () => this.getExcludedDomains(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
}; };
@ -71,8 +75,10 @@ export default class NotificationBackground {
private folderService: FolderService, private folderService: FolderService,
private stateService: BrowserStateService, private stateService: BrowserStateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logService: LogService, private logService: LogService,
private themeStateService: ThemeStateService,
) {} ) {}
async init() { async init() {
@ -99,6 +105,13 @@ export default class NotificationBackground {
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$); return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
} }
/**
* Gets the neverDomains setting from the domain settings service.
*/
async getExcludedDomains(): Promise<NeverDomains> {
return await firstValueFrom(this.domainSettingsService.neverDomains$);
}
/** /**
* Checks the notification queue for any messages that need to be sent to the * Checks the notification queue for any messages that need to be sent to the
* specified tab. If no tab is specified, the current tab will be used. * specified tab. If no tab is specified, the current tab will be used.
@ -154,7 +167,7 @@ export default class NotificationBackground {
const notificationType = notificationQueueMessage.type; const notificationType = notificationQueueMessage.type;
const typeData: Record<string, any> = { const typeData: Record<string, any> = {
isVaultLocked: notificationQueueMessage.wasVaultLocked, isVaultLocked: notificationQueueMessage.wasVaultLocked,
theme: await this.stateService.getTheme(), theme: await firstValueFrom(this.themeStateService.selectedTheme$),
}; };
switch (notificationType) { switch (notificationType) {

View File

@ -1,4 +1,5 @@
import { mock, mockReset } from "jest-mock-extended"; import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
@ -10,6 +11,7 @@ import { AutofillSettingsService } from "@bitwarden/common/autofill/services/aut
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { SettingsService } from "@bitwarden/common/services/settings.service"; import { SettingsService } from "@bitwarden/common/services/settings.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@ -17,8 +19,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
import BrowserPlatformUtilsService from "../../platform/services/browser-platform-utils.service";
import { BrowserStateService } from "../../platform/services/browser-state.service"; import { BrowserStateService } from "../../platform/services/browser-state.service";
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
import { AutofillService } from "../services/abstractions/autofill.service"; import { AutofillService } from "../services/abstractions/autofill.service";
import { import {
createAutofillPageDetailsMock, createAutofillPageDetailsMock,
@ -53,6 +55,7 @@ describe("OverlayBackground", () => {
const autofillSettingsService = mock<AutofillSettingsService>(); const autofillSettingsService = mock<AutofillSettingsService>();
const i18nService = mock<I18nService>(); const i18nService = mock<I18nService>();
const platformUtilsService = mock<BrowserPlatformUtilsService>(); const platformUtilsService = mock<BrowserPlatformUtilsService>();
const themeStateService = mock<ThemeStateService>();
const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => {
const { initList, initButton } = options; const { initList, initButton } = options;
if (initButton) { if (initButton) {
@ -79,12 +82,15 @@ describe("OverlayBackground", () => {
autofillSettingsService, autofillSettingsService,
i18nService, i18nService,
platformUtilsService, platformUtilsService,
themeStateService,
); );
jest jest
.spyOn(overlayBackground as any, "getOverlayVisibility") .spyOn(overlayBackground as any, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
themeStateService.selectedTheme$ = of(ThemeType.Light);
void overlayBackground.init(); void overlayBackground.init();
}); });
@ -993,7 +999,7 @@ describe("OverlayBackground", () => {
}); });
it("gets the system theme", async () => { it("gets the system theme", async () => {
jest.spyOn(overlayBackground["stateService"], "getTheme").mockResolvedValue(ThemeType.System); themeStateService.selectedTheme$ = of(ThemeType.System);
await initOverlayElementPorts({ initList: true, initButton: false }); await initOverlayElementPorts({ initList: true, initButton: false });
await flushPromises(); await flushPromises();

View File

@ -11,6 +11,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
@ -96,6 +97,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private themeStateService: ThemeStateService,
) { ) {
this.iconsServerUrl = this.environmentService.getIconsUrl(); this.iconsServerUrl = this.environmentService.getIconsUrl();
} }
@ -695,7 +697,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
authStatus: await this.getAuthStatus(), authStatus: await this.getAuthStatus(),
styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`),
theme: await this.stateService.getTheme(), theme: await firstValueFrom(this.themeStateService.selectedTheme$),
translations: this.getTranslations(), translations: this.getTranslations(),
ciphers: isOverlayListPort ? this.getOverlayCipherData() : null, ciphers: isOverlayListPort ? this.getOverlayCipherData() : null,
}); });

View File

@ -6,10 +6,6 @@ import {
EventCollectionServiceInitOptions, EventCollectionServiceInitOptions,
eventCollectionServiceFactory, eventCollectionServiceFactory,
} from "../../../background/service-factories/event-collection-service.factory"; } from "../../../background/service-factories/event-collection-service.factory";
import {
settingsServiceFactory,
SettingsServiceInitOptions,
} from "../../../background/service-factories/settings-service.factory";
import { import {
CachedServices, CachedServices,
factory, factory,
@ -38,6 +34,10 @@ import {
AutofillSettingsServiceInitOptions, AutofillSettingsServiceInitOptions,
autofillSettingsServiceFactory, autofillSettingsServiceFactory,
} from "./autofill-settings-service.factory"; } from "./autofill-settings-service.factory";
import {
DomainSettingsServiceInitOptions,
domainSettingsServiceFactory,
} from "./domain-settings-service.factory";
type AutoFillServiceOptions = FactoryOptions; type AutoFillServiceOptions = FactoryOptions;
@ -48,8 +48,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
TotpServiceInitOptions & TotpServiceInitOptions &
EventCollectionServiceInitOptions & EventCollectionServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
SettingsServiceInitOptions & UserVerificationServiceInitOptions &
UserVerificationServiceInitOptions; DomainSettingsServiceInitOptions;
export function autofillServiceFactory( export function autofillServiceFactory(
cache: { autofillService?: AbstractAutoFillService } & CachedServices, cache: { autofillService?: AbstractAutoFillService } & CachedServices,
@ -67,7 +67,7 @@ export function autofillServiceFactory(
await totpServiceFactory(cache, opts), await totpServiceFactory(cache, opts),
await eventCollectionServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts),
), ),
); );

View File

@ -0,0 +1,25 @@
import { DefaultDomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
export type DomainSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
export function domainSettingsServiceFactory(
cache: { domainSettingsService?: DefaultDomainSettingsService } & CachedServices,
opts: DomainSettingsServiceInitOptions,
): Promise<DefaultDomainSettingsService> {
return factory(
cache,
"domainSettingsService",
opts,
async () => new DefaultDomainSettingsService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -1,8 +1,8 @@
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
@ -73,7 +73,7 @@ export default class WebRequestBackground {
const ciphers = await this.cipherService.getAllDecryptedForUrl( const ciphers = await this.cipherService.getAllDecryptedForUrl(
domain, domain,
null, null,
UriMatchType.Host, UriMatchStrategy.Host,
); );
if (ciphers == null || ciphers.length !== 1) { if (ciphers == null || ciphers.length !== 1) {
error(); error();

View File

@ -6,7 +6,7 @@ import AutofillField from "../models/autofill-field";
import { WatchedForm } from "../models/watched-form"; import { WatchedForm } from "../models/watched-form";
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
import { FormData } from "../services/abstractions/autofill.service"; import { FormData } from "../services/abstractions/autofill.service";
import { GlobalSettings, UserSettings } from "../types"; import { UserSettings } from "../types";
import { import {
getFromLocalStorage, getFromLocalStorage,
sendExtensionMessage, sendExtensionMessage,
@ -94,10 +94,11 @@ async function loadNotificationBar() {
"bgGetEnableChangedPasswordPrompt", "bgGetEnableChangedPasswordPrompt",
); );
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt"); const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains");
let showNotificationBar = true; let showNotificationBar = true;
// Look up the active user id from storage // Look up the active user id from storage
const activeUserIdKey = "activeUserId"; const activeUserIdKey = "activeUserId";
const globalStorageKey = "global";
let activeUserId: string; let activeUserId: string;
const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey); const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey);
@ -109,9 +110,6 @@ async function loadNotificationBar() {
const userSettingsStorageValue = await getFromLocalStorage(activeUserId); const userSettingsStorageValue = await getFromLocalStorage(activeUserId);
if (userSettingsStorageValue[activeUserId]) { if (userSettingsStorageValue[activeUserId]) {
const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings;
const globalSettings: GlobalSettings = (await getFromLocalStorage(globalStorageKey))[
globalStorageKey
];
// Do not show the notification bar on the Bitwarden vault // Do not show the notification bar on the Bitwarden vault
// because they can add logins and change passwords there // because they can add logins and change passwords there
@ -122,8 +120,8 @@ async function loadNotificationBar() {
// show the notification bar on (for login detail collection or password change). // show the notification bar on (for login detail collection or password change).
// It is managed in the Settings > Excluded Domains page in the browser extension. // It is managed in the Settings > Excluded Domains page in the browser extension.
// Example: '{"bitwarden.com":null}' // Example: '{"bitwarden.com":null}'
const excludedDomainsDict = globalSettings.neverDomains;
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) { if (!excludedDomains || !(window.location.hostname in excludedDomains)) {
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) { if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page) // If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
handlePageChange(); handlePageChange();

View File

@ -1,14 +1,16 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; 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 { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
@ -28,16 +30,15 @@ export class AutofillComponent implements OnInit {
enableAutoFillOnPageLoad = false; enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false; autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[]; autoFillOnPageLoadOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
autofillKeyboardHelperText: string; autofillKeyboardHelperText: string;
accountSwitcherEnabled = false; accountSwitcherEnabled = false;
constructor( constructor(
private stateService: StateService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private autofillService: AutofillService, private autofillService: AutofillService,
private dialogService: DialogService, private dialogService: DialogService,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
@ -61,12 +62,12 @@ export class AutofillComponent implements OnInit {
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false }, { name: i18nService.t("autoFillOnPageLoadNo"), value: false },
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.accountSwitcherEnabled = enableAccountSwitching(); this.accountSwitcherEnabled = enableAccountSwitching();
@ -94,8 +95,10 @@ export class AutofillComponent implements OnInit {
this.autofillSettingsService.autofillOnPageLoadDefault$, this.autofillSettingsService.autofillOnPageLoadDefault$,
); );
const defaultUriMatch = await this.stateService.getDefaultUriMatch(); const defaultUriMatch = await firstValueFrom(
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); const command = await this.platformUtilsService.getAutofillKeyboardShortcut();
await this.setAutofillKeyboardHelperText(command); await this.setAutofillKeyboardHelperText(command);
@ -119,7 +122,7 @@ export class AutofillComponent implements OnInit {
} }
async saveDefaultUriMatch() { async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch); await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch);
} }
private async setAutofillKeyboardHelperText(command: string) { private async setAutofillKeyboardHelperText(command: string) {

View File

@ -1,4 +1,5 @@
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import AutofillField from "../../models/autofill-field"; import AutofillField from "../../models/autofill-field";
@ -40,7 +41,7 @@ export interface GenerateFillScriptOptions {
allowTotpAutofill: boolean; allowTotpAutofill: boolean;
cipher: CipherView; cipher: CipherView;
tabUrl: string; tabUrl: string;
defaultUriMatch: UriMatchType; defaultUriMatch: UriMatchStrategySetting;
} }
export abstract class AutofillService { export abstract class AutofillService {

View File

@ -1,19 +1,25 @@
import { mock, mockReset } from "jest-mock-extended"; import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { EventType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { import {
FieldType, DefaultDomainSettingsService,
LinkedIdType, DomainSettingsService,
LoginLinkedId, } from "@bitwarden/common/autofill/services/domain-settings.service";
UriMatchType, import { EventType } from "@bitwarden/common/enums";
CipherType, import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
} from "@bitwarden/common/vault/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import {
FakeStateProvider,
FakeAccountService,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -47,15 +53,24 @@ import {
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants"; import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
import AutofillService from "./autofill.service"; import AutofillService from "./autofill.service";
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
describe("AutofillService", () => { describe("AutofillService", () => {
let autofillService: AutofillService; let autofillService: AutofillService;
const cipherService = mock<CipherService>(); const cipherService = mock<CipherService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const autofillSettingsService = mock<AutofillSettingsService>(); const autofillSettingsService = mock<AutofillSettingsService>();
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
let domainSettingsService: DomainSettingsService;
const totpService = mock<TotpService>(); const totpService = mock<TotpService>();
const eventCollectionService = mock<EventCollectionService>(); const eventCollectionService = mock<EventCollectionService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const settingsService = mock<SettingsService>();
const userVerificationService = mock<UserVerificationService>(); const userVerificationService = mock<UserVerificationService>();
beforeEach(() => { beforeEach(() => {
@ -66,9 +81,12 @@ describe("AutofillService", () => {
totpService, totpService,
eventCollectionService, eventCollectionService,
logService, logService,
settingsService, domainSettingsService,
userVerificationService, userVerificationService,
); );
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
}); });
afterEach(() => { afterEach(() => {
@ -407,6 +425,8 @@ describe("AutofillService", () => {
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true); autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
autofillOptions.cipher.login.username = "username"; autofillOptions.cipher.login.username = "username";
autofillOptions.cipher.login.password = "password"; autofillOptions.cipher.login.password = "password";
jest.spyOn(autofillService, "getDefaultUriMatchStrategy").mockResolvedValue(0);
}); });
describe("given a set of autofill options that are incomplete", () => { describe("given a set of autofill options that are incomplete", () => {
@ -468,7 +488,6 @@ describe("AutofillService", () => {
it("will autofill login data for a page", async () => { it("will autofill login data for a page", async () => {
jest.spyOn(stateService, "getCanAccessPremium"); jest.spyOn(stateService, "getCanAccessPremium");
jest.spyOn(stateService, "getDefaultUriMatch");
jest.spyOn(autofillService as any, "generateFillScript"); jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript"); jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info"); jest.spyOn(logService, "info");
@ -479,7 +498,7 @@ describe("AutofillService", () => {
const currentAutofillPageDetails = autofillOptions.pageDetails[0]; const currentAutofillPageDetails = autofillOptions.pageDetails[0];
expect(stateService.getCanAccessPremium).toHaveBeenCalled(); expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(stateService.getDefaultUriMatch).toHaveBeenCalled(); expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled();
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
currentAutofillPageDetails.details, currentAutofillPageDetails.details,
{ {
@ -1488,7 +1507,7 @@ describe("AutofillService", () => {
}; };
defaultLoginUriView = mock<LoginUriView>({ defaultLoginUriView = mock<LoginUriView>({
uri: "https://www.example.com", uri: "https://www.example.com",
match: UriMatchType.Domain, match: UriMatchStrategy.Domain,
}); });
options = createGenerateFillScriptOptionsMock(); options = createGenerateFillScriptOptionsMock();
options.cipher.login = mock<LoginView>({ options.cipher.login = mock<LoginView>({
@ -1559,13 +1578,13 @@ describe("AutofillService", () => {
]); ]);
}); });
it("skips adding any login uri views that have a UriMatchType of Never to the list of saved urls", async () => { it("skips adding any login uri views that have a UriMatchStrategySetting of Never to the list of saved urls", async () => {
const secondUriView = mock<LoginUriView>({ const secondUriView = mock<LoginUriView>({
uri: "https://www.second-example.com", uri: "https://www.second-example.com",
}); });
const thirdUriView = mock<LoginUriView>({ const thirdUriView = mock<LoginUriView>({
uri: "https://www.third-example.com", uri: "https://www.third-example.com",
match: UriMatchType.Never, match: UriMatchStrategy.Never,
}); });
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView]; options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
@ -2752,31 +2771,32 @@ describe("AutofillService", () => {
}); });
describe("inUntrustedIframe", () => { describe("inUntrustedIframe", () => {
it("returns a false value if the passed pageUrl is equal to the options tabUrl", () => { it("returns a false value if the passed pageUrl is equal to the options tabUrl", async () => {
const pageUrl = "https://www.example.com"; const pageUrl = "https://www.example.com";
const tabUrl = "https://www.example.com"; const tabUrl = "https://www.example.com";
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService, "getEquivalentDomains");
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).not.toHaveBeenCalled();
expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled(); expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled();
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("returns a false value if the passed pageUrl matches the domain of the tabUrl", () => { it("returns a false value if the passed pageUrl matches the domain of the tabUrl", async () => {
const pageUrl = "https://subdomain.example.com"; const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.example.com"; const tabUrl = "https://www.example.com";
const equivalentDomains = new Set(["example.com"]); const equivalentDomains = new Set([
"ejemplo.es",
"example.co.uk",
"example.com",
"exampleapp.com",
]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,
@ -2785,17 +2805,21 @@ describe("AutofillService", () => {
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", () => { it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", async () => {
const equivalentDomains = new Set([
"ejemplo.es",
"example.co.uk",
"example.com",
"exampleapp.com",
]);
domainSettingsService.equivalentDomains$ = of([["not-example.com"]]);
const pageUrl = "https://subdomain.example.com"; const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.not-example.com"; const tabUrl = "https://www.not-example.com";
const equivalentDomains = new Set(["not-example.com"]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,

View File

@ -1,15 +1,19 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; 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 { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import {
UriMatchStrategySetting,
UriMatchStrategy,
} from "@bitwarden/common/models/domain/domain-service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { FieldType, UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@ -48,7 +52,7 @@ export default class AutofillService implements AutofillServiceInterface {
private totpService: TotpService, private totpService: TotpService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private logService: LogService, private logService: LogService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
) {} ) {}
@ -215,6 +219,13 @@ export default class AutofillService implements AutofillServiceInterface {
return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$); return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$);
} }
/**
* Gets the default URI match strategy setting from the domain settings service.
*/
async getDefaultUriMatchStrategy(): Promise<UriMatchStrategySetting> {
return await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
}
/** /**
* Autofill a given tab with a given login item * Autofill a given tab with a given login item
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item * @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
@ -229,7 +240,7 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: string | null = null; let totp: string | null = null;
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await this.stateService.getCanAccessPremium();
const defaultUriMatch = (await this.stateService.getDefaultUriMatch()) ?? UriMatchType.Domain; const defaultUriMatch = await this.getDefaultUriMatchStrategy();
if (!canAccessPremium) { if (!canAccessPremium) {
options.cipher.login.totp = null; options.cipher.login.totp = null;
@ -579,9 +590,9 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: AutofillField = null; let totp: AutofillField = null;
const login = options.cipher.login; const login = options.cipher.login;
fillScript.savedUrls = fillScript.savedUrls =
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? []; login?.uris?.filter((u) => u.match != UriMatchStrategy.Never).map((u) => u.uri) ?? [];
fillScript.untrustedIframe = this.inUntrustedIframe(pageDetails.url, options); fillScript.untrustedIframe = await this.inUntrustedIframe(pageDetails.url, options);
let passwordFields = AutofillService.loadPasswordFields( let passwordFields = AutofillService.loadPasswordFields(
pageDetails, pageDetails,
@ -1066,7 +1077,10 @@ export default class AutofillService implements AutofillServiceInterface {
* @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise * @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
* @private * @private
*/ */
private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean { private async inUntrustedIframe(
pageUrl: string,
options: GenerateFillScriptOptions,
): Promise<boolean> {
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe // If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
// This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway // This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway
if (pageUrl === options.tabUrl) { if (pageUrl === options.tabUrl) {
@ -1076,7 +1090,9 @@ export default class AutofillService implements AutofillServiceInterface {
// Check the pageUrl against cipher URIs using the configured match detection. // Check the pageUrl against cipher URIs using the configured match detection.
// Remember: if we are in this function, the tabUrl already matches a saved URI for the login. // Remember: if we are in this function, the tabUrl already matches a saved URI for the login.
// We need to verify the pageUrl also matches. // We need to verify the pageUrl also matches.
const equivalentDomains = this.settingsService.getEquivalentDomains(pageUrl); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(pageUrl),
);
const matchesUri = options.cipher.login.matchesUri( const matchesUri = options.cipher.login.matchesUri(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,

View File

@ -1,8 +1,9 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -112,7 +113,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr
allowTotpAutofill: false, allowTotpAutofill: false,
cipher: mock<CipherView>(), cipher: mock<CipherView>(),
tabUrl: "https://jest-testing-website.com", tabUrl: "https://jest-testing-website.com",
defaultUriMatch: UriMatchType.Domain, defaultUriMatch: UriMatchStrategy.Domain,
...customFields, ...customFields,
}; };
} }

View File

@ -1,5 +1,4 @@
import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@ -32,15 +31,10 @@ export type UserSettings = {
utcDate: string; utcDate: string;
version: string; version: string;
}; };
settings: {
equivalentDomains: string[][];
};
vaultTimeout: number; vaultTimeout: number;
vaultTimeoutAction: VaultTimeoutAction; vaultTimeoutAction: VaultTimeoutAction;
}; };
export type GlobalSettings = Pick<GlobalState, "neverDomains">;
/** /**
* A HTMLElement (usually a form element) with additional custom properties added by this script * A HTMLElement (usually a form element) with additional custom properties added by this script
*/ */

View File

@ -56,6 +56,10 @@ import {
BadgeSettingsServiceAbstraction, BadgeSettingsServiceAbstraction,
BadgeSettingsService, BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service"; } from "@bitwarden/common/autofill/services/badge-settings.service";
import {
DomainSettingsService,
DefaultDomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { import {
UserNotificationSettingsService, UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction, UserNotificationSettingsServiceAbstraction,
@ -112,6 +116,7 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */ /* eslint-enable import/no-restricted-paths */
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service"; import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service"; import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service"; import { AuditService } from "@bitwarden/common/services/audit.service";
@ -195,13 +200,14 @@ import { BrowserStateService as StateServiceAbstraction } from "../platform/serv
import { BrowserConfigService } from "../platform/services/browser-config.service"; import { BrowserConfigService } from "../platform/services/browser-config.service";
import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import { BrowserI18nService } from "../platform/services/browser-i18n.service";
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
import BrowserMessagingService from "../platform/services/browser-messaging.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
import { BrowserStateService } from "../platform/services/browser-state.service"; import { BrowserStateService } from "../platform/services/browser-state.service";
import I18nService from "../platform/services/i18n.service";
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserSendService } from "../services/browser-send.service"; import { BrowserSendService } from "../services/browser-send.service";
@ -258,6 +264,7 @@ export default class MainBackground {
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
badgeSettingsService: BadgeSettingsServiceAbstraction; badgeSettingsService: BadgeSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
systemService: SystemServiceAbstraction; systemService: SystemServiceAbstraction;
eventCollectionService: EventCollectionServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction;
eventUploadService: EventUploadServiceAbstraction; eventUploadService: EventUploadServiceAbstraction;
@ -352,7 +359,7 @@ export default class MainBackground {
this.cryptoFunctionService = new WebCryptoFunctionService(self); this.cryptoFunctionService = new WebCryptoFunctionService(self);
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
this.storageService = new BrowserLocalStorageService(); this.storageService = new BrowserLocalStorageService();
this.secureStorageService = new BrowserLocalStorageService(); this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
this.memoryStorageService = BrowserApi.isManifestVersion(3) this.memoryStorageService = BrowserApi.isManifestVersion(3)
? new LocalBackedSessionStorageService( ? new LocalBackedSessionStorageService(
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
@ -438,31 +445,16 @@ export default class MainBackground {
migrationRunner, migrationRunner,
); );
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
this.platformUtilsService = new BrowserPlatformUtilsService( this.platformUtilsService = new BackgroundPlatformUtilsService(
this.messagingService, this.messagingService,
(clipboardValue, clearMs) => { (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
if (this.systemService != null) { async () => this.biometricUnlock(),
// 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.systemService.clearClipboard(clipboardValue, clearMs);
}
},
async () => {
if (this.nativeMessagingBackground != null) {
const promise = this.nativeMessagingBackground.getResponse();
try {
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
} catch (e) {
return Promise.reject(e);
}
return promise.then((result) => result.response === "unlocked");
}
},
self, self,
); );
this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService);
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.cryptoService = new BrowserCryptoService( this.cryptoService = new BrowserCryptoService(
this.keyGenerationService, this.keyGenerationService,
this.cryptoFunctionService, this.cryptoFunctionService,
@ -475,7 +467,7 @@ export default class MainBackground {
this.biometricStateService, this.biometricStateService,
); );
this.tokenService = new TokenService(this.stateService); this.tokenService = new TokenService(this.stateService);
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.globalStateProvider);
this.apiService = new ApiService( this.apiService = new ApiService(
this.tokenService, this.tokenService,
this.platformUtilsService, this.platformUtilsService,
@ -483,6 +475,7 @@ export default class MainBackground {
this.appIdService, this.appIdService,
(expired: boolean) => this.logout(expired), (expired: boolean) => this.logout(expired),
); );
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.settingsService = new BrowserSettingsService(this.stateService); this.settingsService = new BrowserSettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
this.cipherFileUploadService = new CipherFileUploadService( this.cipherFileUploadService = new CipherFileUploadService(
@ -588,6 +581,7 @@ export default class MainBackground {
this.policyService, this.policyService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.globalStateProvider,
); );
this.ssoLoginService = new SsoLoginService(this.stateProvider); this.ssoLoginService = new SsoLoginService(this.stateProvider);
@ -607,7 +601,7 @@ export default class MainBackground {
this.cipherService = new CipherService( this.cipherService = new CipherService(
this.cryptoService, this.cryptoService,
this.settingsService, this.domainSettingsService,
this.apiService, this.apiService,
this.i18nService, this.i18nService,
this.searchService, this.searchService,
@ -694,7 +688,7 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService( this.syncService = new SyncService(
this.apiService, this.apiService,
this.settingsService, this.domainSettingsService,
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.cryptoService, this.cryptoService,
@ -731,7 +725,7 @@ export default class MainBackground {
this.totpService, this.totpService,
this.eventCollectionService, this.eventCollectionService,
this.logService, this.logService,
this.settingsService, this.domainSettingsService,
this.userVerificationService, this.userVerificationService,
); );
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
@ -795,6 +789,7 @@ export default class MainBackground {
this.authService, this.authService,
this.stateService, this.stateService,
this.vaultSettingsService, this.vaultSettingsService,
this.domainSettingsService,
this.logService, this.logService,
); );
@ -864,8 +859,10 @@ export default class MainBackground {
this.folderService, this.folderService,
this.stateService, this.stateService,
this.userNotificationSettingsService, this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService, this.environmentService,
this.logService, this.logService,
themeStateService,
); );
this.overlayBackground = new OverlayBackground( this.overlayBackground = new OverlayBackground(
this.cipherService, this.cipherService,
@ -877,6 +874,7 @@ export default class MainBackground {
this.autofillSettingsService, this.autofillSettingsService,
this.i18nService, this.i18nService,
this.platformUtilsService, this.platformUtilsService,
themeStateService,
); );
this.filelessImporterBackground = new FilelessImporterBackground( this.filelessImporterBackground = new FilelessImporterBackground(
this.configService, this.configService,
@ -969,7 +967,7 @@ export default class MainBackground {
await this.stateService.init(); await this.stateService.init();
await this.vaultTimeoutService.init(true); await this.vaultTimeoutService.init(true);
await (this.i18nService as BrowserI18nService).init(); await (this.i18nService as I18nService).init();
await (this.eventUploadService as EventUploadService).init(true); await (this.eventUploadService as EventUploadService).init(true);
await this.runtimeBackground.init(); await this.runtimeBackground.init();
await this.notificationBackground.init(); await this.notificationBackground.init();
@ -1097,7 +1095,6 @@ export default class MainBackground {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0), userId), this.syncService.setLastSync(new Date(0), userId),
this.cryptoService.clearKeys(userId), this.cryptoService.clearKeys(userId),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),
@ -1222,6 +1219,23 @@ export default class MainBackground {
} }
} }
async clearClipboard(clipboardValue: string, clearMs: number) {
if (this.systemService != null) {
await this.systemService.clearClipboard(clipboardValue, clearMs);
}
}
async biometricUnlock(): Promise<boolean> {
if (this.nativeMessagingBackground == null) {
return false;
}
const responsePromise = this.nativeMessagingBackground.getResponse();
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
const response = await responsePromise;
return response.response === "unlocked";
}
private async fullSync(override = false) { private async fullSync(override = false) {
const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const syncInternal = 6 * 60 * 60 * 1000; // 6 hours
const lastSync = await this.syncService.getLastSync(); const lastSync = await this.syncService.getLastSync();

View File

@ -122,7 +122,7 @@ export class NativeMessagingBackground {
break; break;
case "disconnected": case "disconnected":
if (this.connecting) { if (this.connecting) {
reject("startDesktop"); reject(new Error("startDesktop"));
} }
this.connected = false; this.connected = false;
this.port.disconnect(); this.port.disconnect();
@ -203,7 +203,7 @@ export class NativeMessagingBackground {
this.connected = false; this.connected = false;
const reason = error != null ? "desktopIntegrationDisabled" : null; const reason = error != null ? "desktopIntegrationDisabled" : null;
reject(reason); reject(new Error(reason));
}); });
}); });
} }

View File

@ -19,7 +19,7 @@ import { AutofillService } from "../autofill/services/abstractions/autofill.serv
import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserApi } from "../platform/browser/browser-api";
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { AbortManager } from "../vault/background/abort-manager"; import { AbortManager } from "../vault/background/abort-manager";
import { Fido2Service } from "../vault/services/abstractions/fido2.service"; import { Fido2Service } from "../vault/services/abstractions/fido2.service";
@ -68,6 +68,7 @@ export default class RuntimeBackground {
"checkFido2FeatureEnabled", "checkFido2FeatureEnabled",
"fido2RegisterCredentialRequest", "fido2RegisterCredentialRequest",
"fido2GetCredentialRequest", "fido2GetCredentialRequest",
"biometricUnlock",
]; ];
if (messagesWithResponse.includes(msg.command)) { if (messagesWithResponse.includes(msg.command)) {
@ -305,6 +306,14 @@ export default class RuntimeBackground {
); );
case "switchAccount": { case "switchAccount": {
await this.main.switchAccount(msg.userId); await this.main.switchAccount(msg.userId);
break;
}
case "clearClipboard": {
await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs);
break;
}
case "biometricUnlock": {
return await this.main.biometricUnlock();
} }
} }
} }

View File

@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.2.1", "version": "2024.3.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0", "minimum_chrome_version": "102.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.2.1", "version": "2024.3.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@ -1,14 +1,15 @@
import { DiskStorageOptions } from "@koa/multer";
import { AppIdService as AbstractAppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService as AbstractAppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { diskStorageServiceFactory } from "./storage-service.factory"; import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
type AppIdServiceFactoryOptions = FactoryOptions; type AppIdServiceFactoryOptions = FactoryOptions;
export type AppIdServiceInitOptions = AppIdServiceFactoryOptions & DiskStorageOptions; export type AppIdServiceInitOptions = AppIdServiceFactoryOptions & GlobalStateProviderInitOptions;
export function appIdServiceFactory( export function appIdServiceFactory(
cache: { appIdService?: AbstractAppIdService } & CachedServices, cache: { appIdService?: AbstractAppIdService } & CachedServices,
@ -18,6 +19,6 @@ export function appIdServiceFactory(
cache, cache,
"appIdService", "appIdService",
opts, opts,
async () => new AppIdService(await diskStorageServiceFactory(cache, opts)), async () => new AppIdService(await globalStateProviderFactory(cache, opts)),
); );
} }

View File

@ -4,6 +4,10 @@ import { I18nService as BaseI18nService } from "@bitwarden/common/platform/servi
import I18nService from "../../services/i18n.service"; import I18nService from "../../services/i18n.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { FactoryOptions, CachedServices, factory } from "./factory-options";
import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
type I18nServiceFactoryOptions = FactoryOptions & { type I18nServiceFactoryOptions = FactoryOptions & {
i18nServiceOptions: { i18nServiceOptions: {
@ -11,7 +15,7 @@ type I18nServiceFactoryOptions = FactoryOptions & {
}; };
}; };
export type I18nServiceInitOptions = I18nServiceFactoryOptions; export type I18nServiceInitOptions = I18nServiceFactoryOptions & GlobalStateProviderInitOptions;
export async function i18nServiceFactory( export async function i18nServiceFactory(
cache: { i18nService?: AbstractI18nService } & CachedServices, cache: { i18nService?: AbstractI18nService } & CachedServices,
@ -21,7 +25,11 @@ export async function i18nServiceFactory(
cache, cache,
"i18nService", "i18nService",
opts, opts,
() => new I18nService(opts.i18nServiceOptions.systemLanguage), async () =>
new I18nService(
opts.i18nServiceOptions.systemLanguage,
await globalStateProviderFactory(cache, opts),
),
); );
if (!(service as BaseI18nService as any).inited) { if (!(service as BaseI18nService as any).inited) {
await (service as BaseI18nService).init(); await (service as BaseI18nService).init();

View File

@ -1,6 +1,6 @@
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import BrowserPlatformUtilsService from "../../services/browser-platform-utils.service"; import { BackgroundPlatformUtilsService } from "../../services/platform-utils/background-platform-utils.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory"; import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory";
@ -25,7 +25,7 @@ export function platformUtilsServiceFactory(
"platformUtilsService", "platformUtilsService",
opts, opts,
async () => async () =>
new BrowserPlatformUtilsService( new BackgroundPlatformUtilsService(
await messagingServiceFactory(cache, opts), await messagingServiceFactory(cache, opts),
opts.platformUtilsServiceOptions.clipboardWriteCallback, opts.platformUtilsServiceOptions.clipboardWriteCallback,
opts.platformUtilsServiceOptions.biometricCallback, opts.platformUtilsServiceOptions.biometricCallback,

View File

@ -3,7 +3,7 @@ import { Observable } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums"; import { DeviceType } from "@bitwarden/common/enums";
import { TabMessage } from "../../types/tab-messages"; import { TabMessage } from "../../types/tab-messages";
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
export class BrowserApi { export class BrowserApi {
static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isWebExtensionsApi: boolean = typeof browser !== "undefined";

View File

@ -17,7 +17,7 @@ import { Account } from "../../models/account";
import IconDetails from "../../vault/background/models/icon-details"; import IconDetails from "../../vault/background/models/icon-details";
import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory"; import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
export type BadgeOptions = { export type BadgeOptions = {
tab?: chrome.tabs.Tab; tab?: chrome.tabs.Tab;

View File

@ -1,20 +0,0 @@
import { ReplaySubject } from "rxjs";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import I18nService from "./i18n.service";
@browserSession
export class BrowserI18nService extends I18nService {
@sessionSync({ initializer: (s: string) => s })
protected _locale: ReplaySubject<string>;
constructor(
systemLanguage: string,
private stateService: StateService,
) {
super(systemLanguage);
}
}

View File

@ -1,12 +1,18 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export default class I18nService extends BaseI18nService { export default class I18nService extends BaseI18nService {
constructor(systemLanguage: string) { constructor(systemLanguage: string, globalStateProvider: GlobalStateProvider) {
super(systemLanguage, null, async (formattedLocale: string) => { super(
// Deprecated systemLanguage,
const file = await fetch(this.localesDirectory + formattedLocale + "/messages.json"); null,
return await file.json(); async (formattedLocale: string) => {
}); // Deprecated
const file = await fetch(this.localesDirectory + formattedLocale + "/messages.json");
return await file.json();
},
globalStateProvider,
);
// Please leave 'en' where it is, as it's our fallback language in case no translation can be found // Please leave 'en' where it is, as it's our fallback language in case no translation can be found
this.supportedTranslationLocales = [ this.supportedTranslationLocales = [

View File

@ -0,0 +1,28 @@
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService {
constructor(
private messagingService: MessagingService,
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
biometricCallback: () => Promise<boolean>,
win: Window & typeof globalThis,
) {
super(clipboardWriteCallback, biometricCallback, win);
}
override showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
this.messagingService.send("showToast", {
text: text,
title: title,
type: type,
options: options,
});
}
}

View File

@ -1,11 +1,26 @@
import { DeviceType } from "@bitwarden/common/enums"; import { DeviceType } from "@bitwarden/common/enums";
import { flushPromises } from "../../autofill/spec/testing-utils"; import { flushPromises } from "../../../autofill/spec/testing-utils";
import { SafariApp } from "../../browser/safariApp"; import { SafariApp } from "../../../browser/safariApp";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../../browser/browser-api";
import BrowserClipboardService from "../browser-clipboard.service";
import BrowserClipboardService from "./browser-clipboard.service"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
import BrowserPlatformUtilsService from "./browser-platform-utils.service";
class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) {
super(clipboardSpy, null, win);
}
showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
throw new Error("Method not implemented.");
}
}
describe("Browser Utils Service", () => { describe("Browser Utils Service", () => {
let browserPlatformUtilsService: BrowserPlatformUtilsService; let browserPlatformUtilsService: BrowserPlatformUtilsService;
@ -13,10 +28,8 @@ describe("Browser Utils Service", () => {
beforeEach(() => { beforeEach(() => {
(window as any).matchMedia = jest.fn().mockReturnValueOnce({}); (window as any).matchMedia = jest.fn().mockReturnValueOnce({});
browserPlatformUtilsService = new BrowserPlatformUtilsService( browserPlatformUtilsService = new TestBrowserPlatformUtilsService(
null,
clipboardWriteCallbackSpy, clipboardWriteCallbackSpy,
null,
window, window,
); );
}); });

View File

@ -1,20 +1,17 @@
import { ClientType, DeviceType } from "@bitwarden/common/enums"; import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { import {
ClipboardOptions, ClipboardOptions,
PlatformUtilsService, PlatformUtilsService,
} from "@bitwarden/common/platform/abstractions/platform-utils.service"; } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SafariApp } from "../../browser/safariApp"; import { SafariApp } from "../../../browser/safariApp";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../../browser/browser-api";
import BrowserClipboardService from "../browser-clipboard.service";
import BrowserClipboardService from "./browser-clipboard.service"; export abstract class BrowserPlatformUtilsService implements PlatformUtilsService {
export default class BrowserPlatformUtilsService implements PlatformUtilsService {
private static deviceCache: DeviceType = null; private static deviceCache: DeviceType = null;
constructor( constructor(
private messagingService: MessagingService,
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
private biometricCallback: () => Promise<boolean>, private biometricCallback: () => Promise<boolean>,
private globalContext: Window | ServiceWorkerGlobalScope, private globalContext: Window | ServiceWorkerGlobalScope,
@ -193,19 +190,12 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
return true; return true;
} }
showToast( abstract showToast(
type: "error" | "success" | "warning" | "info", type: "error" | "success" | "warning" | "info",
title: string, title: string,
text: string | string[], text: string | string[],
options?: any, options?: any,
): void { ): void;
this.messagingService.send("showToast", {
text: text,
title: title,
type: type,
options: options,
});
}
isDev(): boolean { isDev(): boolean {
return process.env.ENV === "development"; return process.env.ENV === "development";
@ -279,11 +269,10 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
async supportsBiometric() { async supportsBiometric() {
const platformInfo = await BrowserApi.getPlatformInfo(); const platformInfo = await BrowserApi.getPlatformInfo();
if (platformInfo.os === "android") { if (platformInfo.os === "mac" || platformInfo.os === "win") {
return false; return true;
} }
return false;
return true;
} }
authenticateBiometric() { authenticateBiometric() {

View File

@ -0,0 +1,40 @@
import { SecurityContext } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { ToastrService } from "ngx-toastr";
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService {
constructor(
private sanitizer: DomSanitizer,
private toastrService: ToastrService,
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
biometricCallback: () => Promise<boolean>,
win: Window & typeof globalThis,
) {
super(clipboardWriteCallback, biometricCallback, win);
}
override showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
if (typeof text === "string") {
// Already in the correct format
} else if (text.length === 1) {
text = text[0];
} else {
let message = "";
text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"),
);
text = message;
options.enableHtml = true;
}
this.toastrService.show(text, title, options, "toast-" + type);
// noop
}
}

View File

@ -1,26 +1,17 @@
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
SecurityContext,
} from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr"; import { ToastrService } from "ngx-toastr";
import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs"; import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserApi } from "../platform/browser/browser-api";
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service";
import { routerTransition } from "./app-routing.animations"; import { routerTransition } from "./app-routing.animations";
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component"; import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
@ -46,11 +37,9 @@ export class AppComponent implements OnInit, OnDestroy {
private i18nService: I18nService, private i18nService: I18nService,
private router: Router, private router: Router,
private stateService: BrowserStateService, private stateService: BrowserStateService,
private messagingService: MessagingService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone, private ngZone: NgZone,
private sanitizer: DomSanitizer, private platformUtilsService: ForegroundPlatformUtilsService,
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService, private dialogService: DialogService,
private browserMessagingApi: ZonedMessageListenerService, private browserMessagingApi: ZonedMessageListenerService,
) {} ) {}
@ -217,31 +206,7 @@ export class AppComponent implements OnInit, OnDestroy {
} }
private showToast(msg: any) { private showToast(msg: any) {
let message = ""; this.platformUtilsService.showToast(msg.type, msg.title, msg.text, msg.options);
const options: Partial<IndividualConfig> = {};
if (typeof msg.text === "string") {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"),
);
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
}
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
} }
private async showDialog(msg: SimpleDialogOptions) { private async showDialog(msg: SimpleDialogOptions) {

View File

@ -1,7 +1,7 @@
import { enableProdMode } from "@angular/core"; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
require("./scss/popup.scss"); require("./scss/popup.scss");
require("./scss/tailwind.css"); require("./scss/tailwind.css");

View File

@ -1,4 +1,5 @@
import { Injectable } from "@angular/core"; import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -19,11 +20,13 @@ export class InitService {
private logService: LogServiceAbstraction, private logService: LogServiceAbstraction,
private themingService: AbstractThemingService, private themingService: AbstractThemingService,
private configService: ConfigService, private configService: ConfigService,
@Inject(DOCUMENT) private document: Document,
) {} ) {}
init() { init() {
return async () => { return async () => {
await this.stateService.init(); await this.stateService.init();
await this.i18nService.init();
if (!BrowserPopupUtils.inPopup(window)) { if (!BrowserPopupUtils.inPopup(window)) {
window.document.body.classList.add("body-full"); window.document.body.classList.add("body-full");
@ -34,7 +37,7 @@ export class InitService {
} }
const htmlEl = window.document.documentElement; const htmlEl = window.document.documentElement;
await this.themingService.monitorThemeChanges(); this.themingService.applyThemeChangesTo(this.document);
htmlEl.classList.add("locale_" + this.i18nService.translationLocale); htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
// Workaround for slow performance on external monitors on Chrome + MacOS // Workaround for slow performance on external monitors on Chrome + MacOS

View File

@ -1,13 +1,15 @@
import { APP_INITIALIZER, LOCALE_ID, NgModule, NgZone } from "@angular/core"; import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { ToastrService } from "ngx-toastr";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { import {
MEMORY_STORAGE, MEMORY_STORAGE,
SECURE_STORAGE, SECURE_STORAGE,
OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_MEMORY_STORAGE,
SYSTEM_THEME_OBSERVABLE,
} from "@bitwarden/angular/services/injection-tokens"; } from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { import {
@ -44,7 +46,6 @@ import {
UserNotificationSettingsService, UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction, UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service"; } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -60,10 +61,7 @@ import {
} from "@bitwarden/common/platform/abstractions/log.service"; } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
StateService as BaseStateServiceAbstraction,
StateService,
} from "@bitwarden/common/platform/abstractions/state.service";
import { import {
AbstractMemoryStorageService, AbstractMemoryStorageService,
AbstractStorageService, AbstractStorageService,
@ -75,7 +73,11 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state"; import {
DerivedStateProvider,
GlobalStateProvider,
StateProvider,
} from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service"; import { SearchService } from "@bitwarden/common/services/search.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
@ -89,14 +91,9 @@ import {
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import {
FolderService as FolderServiceAbstraction,
InternalFolderService,
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import { ImportServiceAbstraction } from "@bitwarden/importer/core";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
@ -112,11 +109,12 @@ import { BrowserStateService as StateServiceAbstraction } from "../../platform/s
import { BrowserConfigService } from "../../platform/services/browser-config.service"; import { BrowserConfigService } from "../../platform/services/browser-config.service";
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service"; import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service";
import { BrowserI18nService } from "../../platform/services/browser-i18n.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service";
import BrowserMessagingService from "../../platform/services/browser-messaging.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service";
import { BrowserStateService } from "../../platform/services/browser-state.service"; import { BrowserStateService } from "../../platform/services/browser-state.service";
import I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { BrowserSendService } from "../../services/browser-send.service"; import { BrowserSendService } from "../../services/browser-send.service";
@ -158,11 +156,6 @@ function getBgService<T>(service: keyof MainBackground) {
DebounceNavigationService, DebounceNavigationService,
DialogService, DialogService,
PopupCloseWarningService, PopupCloseWarningService,
{
provide: LOCALE_ID,
useFactory: () => getBgService<I18nServiceAbstraction>("i18nService")().translationLocale,
deps: [],
},
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: (initService: InitService) => initService.init(), useFactory: (initService: InitService) => initService.init(),
@ -219,17 +212,6 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: () => new WebCryptoFunctionService(window), useFactory: () => new WebCryptoFunctionService(window),
deps: [], deps: [],
}, },
{
provide: InternalFolderService,
useExisting: FolderServiceAbstraction,
},
{
provide: FolderApiServiceAbstraction,
useFactory: (folderService: InternalFolderService, apiService: ApiService) => {
return new FolderApiService(folderService, apiService);
},
deps: [InternalFolderService, ApiService],
},
{ {
provide: CollectionService, provide: CollectionService,
useFactory: getBgService<CollectionService>("collectionService"), useFactory: getBgService<CollectionService>("collectionService"),
@ -254,10 +236,10 @@ function getBgService<T>(service: keyof MainBackground) {
{ provide: TokenService, useFactory: getBgService<TokenService>("tokenService"), deps: [] }, { provide: TokenService, useFactory: getBgService<TokenService>("tokenService"), deps: [] },
{ {
provide: I18nServiceAbstraction, provide: I18nServiceAbstraction,
useFactory: (stateService: BrowserStateService) => { useFactory: (globalStateProvider: GlobalStateProvider) => {
return new BrowserI18nService(BrowserApi.getUILanguage(), stateService); return new I18nService(BrowserApi.getUILanguage(), globalStateProvider);
}, },
deps: [StateService], deps: [GlobalStateProvider],
}, },
{ {
provide: CryptoService, provide: CryptoService,
@ -295,8 +277,32 @@ function getBgService<T>(service: keyof MainBackground) {
}, },
{ {
provide: PlatformUtilsService, provide: PlatformUtilsService,
useFactory: getBgService<PlatformUtilsService>("platformUtilsService"), useExisting: ForegroundPlatformUtilsService,
deps: [], },
{
provide: ForegroundPlatformUtilsService,
useClass: ForegroundPlatformUtilsService,
useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => {
return new ForegroundPlatformUtilsService(
sanitizer,
toastrService,
(clipboardValue: string, clearMs: number) => {
void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs });
},
async () => {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
error: string;
}>("biometricUnlock");
if (!response.result) {
throw response.error;
}
return response.result;
},
window,
);
},
deps: [DomSanitizer, ToastrService],
}, },
{ {
provide: PasswordStrengthServiceAbstraction, provide: PasswordStrengthServiceAbstraction,
@ -353,7 +359,6 @@ function getBgService<T>(service: keyof MainBackground) {
useClass: BrowserLocalStorageService, useClass: BrowserLocalStorageService,
deps: [], deps: [],
}, },
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
{ {
provide: AutofillService, provide: AutofillService,
useFactory: getBgService<AutofillService>("autofillService"), useFactory: getBgService<AutofillService>("autofillService"),
@ -394,11 +399,6 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<NotificationsService>("notificationsService"), useFactory: getBgService<NotificationsService>("notificationsService"),
deps: [], deps: [],
}, },
{
provide: LogServiceAbstraction,
useFactory: getBgService<ConsoleLogService>("logService"),
deps: [],
},
{ {
provide: OrganizationService, provide: OrganizationService,
useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => { useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => {
@ -421,7 +421,7 @@ function getBgService<T>(service: keyof MainBackground) {
}, },
{ {
provide: SECURE_STORAGE, provide: SECURE_STORAGE,
useFactory: getBgService<AbstractStorageService>("secureStorageService"), useExisting: AbstractStorageService, // Secure storage is not available in the browser, so we use normal storage instead and warn users when it is used.
}, },
{ {
provide: MEMORY_STORAGE, provide: MEMORY_STORAGE,
@ -488,11 +488,8 @@ function getBgService<T>(service: keyof MainBackground) {
deps: [StateServiceAbstraction], deps: [StateServiceAbstraction],
}, },
{ {
provide: AbstractThemingService, provide: SYSTEM_THEME_OBSERVABLE,
useFactory: ( useFactory: (platformUtilsService: PlatformUtilsService) => {
stateService: StateServiceAbstraction,
platformUtilsService: PlatformUtilsService,
) => {
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
// In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. // In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
let windowContext = window; let windowContext = window;
@ -501,9 +498,9 @@ function getBgService<T>(service: keyof MainBackground) {
windowContext = backgroundWindow; windowContext = backgroundWindow;
} }
return new ThemingService(stateService, windowContext, document); return AngularThemingService.createSystemThemeFromWindow(windowContext);
}, },
deps: [StateServiceAbstraction, PlatformUtilsService], deps: [PlatformUtilsService],
}, },
{ {
provide: ConfigService, provide: ConfigService,

View File

@ -1,6 +1,8 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -30,6 +32,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService, private i18nService: I18nService,
private router: Router, private router: Router,
private broadcasterService: BroadcasterService, private broadcasterService: BroadcasterService,
@ -40,7 +43,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
const savedDomains = await this.stateService.getNeverDomains(); const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
if (savedDomains) { if (savedDomains) {
for (const uri of Object.keys(savedDomains)) { for (const uri of Object.keys(savedDomains)) {
this.excludedDomains.push({ uri: uri, showCurrentUris: false }); this.excludedDomains.push({ uri: uri, showCurrentUris: false });
@ -107,7 +110,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
} }
await this.stateService.setNeverDomains(savedDomains); await this.domainSettingsService.setNeverDomains(savedDomains);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/settings"]); this.router.navigate(["/tabs/settings"]);

View File

@ -1,18 +1,22 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types"; import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { enableAccountSwitching } from "../../platform/flags"; import { enableAccountSwitching } from "../../platform/flags";
@ -36,7 +40,7 @@ export class OptionsComponent implements OnInit {
showClearClipboard = true; showClearClipboard = true;
theme: ThemeType; theme: ThemeType;
themeOptions: any[]; themeOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
clearClipboard: ClearClipboardDelaySetting; clearClipboard: ClearClipboardDelaySetting;
clearClipboardOptions: any[]; clearClipboardOptions: any[];
@ -50,9 +54,10 @@ export class OptionsComponent implements OnInit {
private stateService: StateService, private stateService: StateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction, private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
private themingService: AbstractThemingService, private themeStateService: ThemeStateService,
private settingsService: SettingsService, private settingsService: SettingsService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
) { ) {
@ -64,12 +69,12 @@ export class OptionsComponent implements OnInit {
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.clearClipboardOptions = [ this.clearClipboardOptions = [
{ name: i18nService.t("never"), value: null }, { name: i18nService.t("never"), value: null },
@ -120,10 +125,12 @@ export class OptionsComponent implements OnInit {
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
this.theme = await this.stateService.getTheme(); this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
const defaultUriMatch = await this.stateService.getDefaultUriMatch(); const defaultUriMatch = await firstValueFrom(
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
} }
@ -179,11 +186,7 @@ export class OptionsComponent implements OnInit {
} }
async saveTheme() { async saveTheme() {
await this.themingService.updateConfiguredTheme(this.theme); await this.themeStateService.setSelectedTheme(this.theme);
}
async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch);
} }
async saveClearClipboard() { async saveClearClipboard() {

View File

@ -397,7 +397,7 @@ export class SettingsComponent implements OnInit {
// Handle connection errors // Handle connection errors
this.form.controls.biometric.setValue(false); this.form.controls.biometric.setValue(false);
const error = BiometricErrors[e as BiometricErrorTypes]; const error = BiometricErrors[e.message as BiometricErrorTypes];
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -1,15 +1,11 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { AccountSettingsSettings } from "@bitwarden/common/platform/models/domain/account";
import { SettingsService } from "@bitwarden/common/services/settings.service"; import { SettingsService } from "@bitwarden/common/services/settings.service";
import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable"; import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable";
@browserSession @browserSession
export class BrowserSettingsService extends SettingsService { export class BrowserSettingsService extends SettingsService {
@sessionSync({ initializer: (obj: string[][]) => obj })
protected _settings: BehaviorSubject<AccountSettingsSettings>;
@sessionSync({ initializer: (b: boolean) => b }) @sessionSync({ initializer: (b: boolean) => b })
protected _disableFavicon: BehaviorSubject<boolean>; protected _disableFavicon: BehaviorSubject<boolean>;
} }

View File

@ -5,6 +5,10 @@ import {
AutofillSettingsServiceInitOptions, AutofillSettingsServiceInitOptions,
autofillSettingsServiceFactory, autofillSettingsServiceFactory,
} from "../../../autofill/background/service_factories/autofill-settings-service.factory"; } from "../../../autofill/background/service_factories/autofill-settings-service.factory";
import {
DomainSettingsServiceInitOptions,
domainSettingsServiceFactory,
} from "../../../autofill/background/service_factories/domain-settings-service.factory";
import { import {
CipherFileUploadServiceInitOptions, CipherFileUploadServiceInitOptions,
cipherFileUploadServiceFactory, cipherFileUploadServiceFactory,
@ -13,10 +17,6 @@ import {
searchServiceFactory, searchServiceFactory,
SearchServiceInitOptions, SearchServiceInitOptions,
} from "../../../background/service-factories/search-service.factory"; } from "../../../background/service-factories/search-service.factory";
import {
SettingsServiceInitOptions,
settingsServiceFactory,
} from "../../../background/service-factories/settings-service.factory";
import { import {
apiServiceFactory, apiServiceFactory,
ApiServiceInitOptions, ApiServiceInitOptions,
@ -51,13 +51,13 @@ type CipherServiceFactoryOptions = FactoryOptions;
export type CipherServiceInitOptions = CipherServiceFactoryOptions & export type CipherServiceInitOptions = CipherServiceFactoryOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
SettingsServiceInitOptions &
ApiServiceInitOptions & ApiServiceInitOptions &
CipherFileUploadServiceInitOptions & CipherFileUploadServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
SearchServiceInitOptions & SearchServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
AutofillSettingsServiceInitOptions & AutofillSettingsServiceInitOptions &
DomainSettingsServiceInitOptions &
EncryptServiceInitOptions & EncryptServiceInitOptions &
ConfigServiceInitOptions; ConfigServiceInitOptions;
@ -72,7 +72,7 @@ export function cipherServiceFactory(
async () => async () =>
new CipherService( new CipherService(
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await searchServiceFactory(cache, opts), await searchServiceFactory(cache, opts),

View File

@ -3,6 +3,7 @@ import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@ -52,6 +53,7 @@ export class Fido2UseBrowserLinkComponent {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
) {} ) {}
@ -89,7 +91,7 @@ export class Fido2UseBrowserLinkComponent {
* @param uri - The domain uri to exclude from future FIDO2 prompts. * @param uri - The domain uri to exclude from future FIDO2 prompts.
*/ */
private async handleDomainExclusion(uri: string) { private async handleDomainExclusion(uri: string) {
const exisitingDomains = await this.stateService.getNeverDomains(); const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri); const validDomain = Utils.getHostname(uri);
const savedDomains: { [name: string]: unknown } = { const savedDomains: { [name: string]: unknown } = {
@ -97,9 +99,7 @@ export class Fido2UseBrowserLinkComponent {
}; };
savedDomains[validDomain] = null; savedDomains[validDomain] = null;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.domainSettingsService.setNeverDomains(savedDomains);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setNeverDomains(savedDomains);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",

View File

@ -5,6 +5,7 @@ import {
combineLatest, combineLatest,
concatMap, concatMap,
filter, filter,
firstValueFrom,
map, map,
Observable, Observable,
Subject, Subject,
@ -13,7 +14,7 @@ import {
} from "rxjs"; } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -72,7 +73,7 @@ export class Fido2Component implements OnInit, OnDestroy {
private cipherService: CipherService, private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private searchService: SearchService, private searchService: SearchService,
private logService: LogService, private logService: LogService,
private dialogService: DialogService, private dialogService: DialogService,
@ -133,7 +134,9 @@ export class Fido2Component implements OnInit, OnDestroy {
concatMap(async (message) => { concatMap(async (message) => {
switch (message.type) { switch (message.type) {
case "ConfirmNewCredentialRequest": { case "ConfirmNewCredentialRequest": {
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.ciphers = (await this.cipherService.getAllDecrypted()).filter( this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
@ -317,7 +320,9 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers, this.ciphers,
); );
} else { } else {
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.displayedCiphers = this.ciphers.filter((cipher) => this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains), cipher.login.matchesUri(this.url, equivalentDomains),
); );

View File

@ -1,6 +1,5 @@
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
@ -138,9 +137,6 @@ const plugins = [
entryModule: "src/popup/app.module#AppModule", entryModule: "src/popup/app.module#AppModule",
sourceMap: true, sourceMap: true,
}), }),
new CleanWebpackPlugin({
cleanAfterEveryBuildPatterns: ["!popup/fonts/**/*"],
}),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
process: "process/browser.js", process: "process/browser.js",
}), }),
@ -244,6 +240,7 @@ const mainConfig = {
output: { output: {
filename: "[name].js", filename: "[name].js",
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, "build"),
clean: true,
}, },
module: { module: {
noParse: /\.wasm$/, noParse: /\.wasm$/,

View File

@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/cli", "name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.2.1", "version": "2024.3.0",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",

View File

@ -273,8 +273,8 @@ export class LoginCommand {
selectedProvider.type === TwoFactorProviderType.Email selectedProvider.type === TwoFactorProviderType.Email
) { ) {
const emailReq = new TwoFactorEmailRequest(); const emailReq = new TwoFactorEmailRequest();
emailReq.email = this.loginStrategyService.email; emailReq.email = await this.loginStrategyService.getEmail();
emailReq.masterPasswordHash = this.loginStrategyService.masterPasswordHash; emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
await this.apiService.postTwoFactorEmail(emailReq); await this.apiService.postTwoFactorEmail(emailReq);
} }

View File

@ -35,6 +35,10 @@ import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.ser
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -190,6 +194,7 @@ export class Main {
pinCryptoService: PinCryptoServiceAbstraction; pinCryptoService: PinCryptoServiceAbstraction;
stateService: StateService; stateService: StateService;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
organizationService: OrganizationService; organizationService: OrganizationService;
providerService: ProviderService; providerService: ProviderService;
twoFactorService: TwoFactorService; twoFactorService: TwoFactorService;
@ -231,7 +236,6 @@ export class Main {
p = path.join(process.env.HOME, ".config/Bitwarden CLI"); p = path.join(process.env.HOME, ".config/Bitwarden CLI");
} }
this.i18nService = new I18nService("en", "./locales");
this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson); this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson);
this.logService = new ConsoleLogService( this.logService = new ConsoleLogService(
this.platformUtilsService.isDev(), this.platformUtilsService.isDev(),
@ -270,6 +274,8 @@ export class Main {
storageServiceProvider, storageServiceProvider,
); );
this.i18nService = new I18nService("en", "./locales", this.globalStateProvider);
this.singleUserStateProvider = new DefaultSingleUserStateProvider( this.singleUserStateProvider = new DefaultSingleUserStateProvider(
storageServiceProvider, storageServiceProvider,
stateEventRegistrarService, stateEventRegistrarService,
@ -332,7 +338,7 @@ export class Main {
this.stateProvider, this.stateProvider,
); );
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.globalStateProvider);
this.tokenService = new TokenService(this.stateService); this.tokenService = new TokenService(this.stateService);
const customUserAgent = const customUserAgent =
@ -357,6 +363,7 @@ export class Main {
this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.containerService = new ContainerService(this.cryptoService, this.encryptService);
this.settingsService = new SettingsService(this.stateService); this.settingsService = new SettingsService(this.stateService);
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
@ -457,6 +464,7 @@ export class Main {
this.policyService, this.policyService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.globalStateProvider,
); );
this.authService = new AuthService( this.authService = new AuthService(
@ -479,7 +487,7 @@ export class Main {
this.cipherService = new CipherService( this.cipherService = new CipherService(
this.cryptoService, this.cryptoService,
this.settingsService, this.domainSettingsService,
this.apiService, this.apiService,
this.i18nService, this.i18nService,
this.searchService, this.searchService,
@ -549,7 +557,7 @@ export class Main {
this.syncService = new SyncService( this.syncService = new SyncService(
this.apiService, this.apiService,
this.settingsService, this.domainSettingsService,
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.cryptoService, this.cryptoService,
@ -645,7 +653,6 @@ export class Main {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId as UserId), this.collectionService.clear(userId as UserId),
@ -665,8 +672,7 @@ export class Main {
await this.stateService.init(); await this.stateService.init();
this.containerService.attachToGlobal(global); this.containerService.attachToGlobal(global);
await this.environmentService.setUrlsFromStorage(); await this.environmentService.setUrlsFromStorage();
const locale = await this.stateService.getLocale(); await this.i18nService.init();
await this.i18nService.init(locale);
this.twoFactorService.init(); this.twoFactorService.init();
this.configService.init(); this.configService.init();

View File

@ -2,18 +2,28 @@ import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export class I18nService extends BaseI18nService { export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) { constructor(
super(systemLanguage, localesDirectory, (formattedLocale: string) => { systemLanguage: string,
const filePath = path.join( localesDirectory: string,
__dirname, globalStateProvider: GlobalStateProvider,
this.localesDirectory + "/" + formattedLocale + "/messages.json", ) {
); super(
const localesJson = fs.readFileSync(filePath, "utf8"); systemLanguage,
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM localesDirectory,
return Promise.resolve(locales); (formattedLocale: string) => {
}); const filePath = path.join(
__dirname,
this.localesDirectory + "/" + formattedLocale + "/messages.json",
);
const localesJson = fs.readFileSync(filePath, "utf8");
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
},
globalStateProvider,
);
this.supportedTranslationLocales = ["en"]; this.supportedTranslationLocales = ["en"];
} }

View File

@ -1,6 +1,5 @@
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const nodeExternals = require("webpack-node-externals"); const nodeExternals = require("webpack-node-externals");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
@ -23,7 +22,6 @@ const moduleRules = [
]; ];
const plugins = [ const plugins = [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [{ from: "./src/locales", to: "locales" }], patterns: [{ from: "./src/locales", to: "locales" }],
}), }),
@ -71,6 +69,7 @@ const webpackConfig = {
output: { output: {
filename: "[name].js", filename: "[name].js",
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, "build"),
clean: true,
}, },
module: { rules: moduleRules }, module: { rules: moduleRules },
plugins: plugins, plugins: plugins,

View File

@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.2.2", "version": "2024.3.1",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",

View File

@ -3,7 +3,6 @@ import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs"; import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators"; import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -21,6 +20,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SetPinComponent } from "../../auth/components/set-pin.component";
@ -116,7 +116,7 @@ export class SettingsComponent implements OnInit {
private messagingService: MessagingService, private messagingService: MessagingService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private modalService: ModalService, private modalService: ModalService,
private themingService: AbstractThemingService, private themeStateService: ThemeStateService,
private settingsService: SettingsService, private settingsService: SettingsService,
private dialogService: DialogService, private dialogService: DialogService,
private userVerificationService: UserVerificationServiceAbstraction, private userVerificationService: UserVerificationServiceAbstraction,
@ -263,8 +263,8 @@ export class SettingsComponent implements OnInit {
await this.stateService.getEnableBrowserIntegrationFingerprint(), await this.stateService.getEnableBrowserIntegrationFingerprint(),
enableDuckDuckGoBrowserIntegration: enableDuckDuckGoBrowserIntegration:
await this.stateService.getEnableDuckDuckGoBrowserIntegration(), await this.stateService.getEnableDuckDuckGoBrowserIntegration(),
theme: await this.stateService.getTheme(), theme: await firstValueFrom(this.themeStateService.selectedTheme$),
locale: (await this.stateService.getLocale()) ?? null, locale: await firstValueFrom(this.i18nService.userSetLocale$),
}; };
this.form.setValue(initialValues, { emitEvent: false }); this.form.setValue(initialValues, { emitEvent: false });
@ -553,11 +553,11 @@ export class SettingsComponent implements OnInit {
} }
async saveLocale() { async saveLocale() {
await this.stateService.setLocale(this.form.value.locale); await this.i18nService.setLocale(this.form.value.locale);
} }
async saveTheme() { async saveTheme() {
await this.themingService.updateConfiguredTheme(this.form.value.theme); await this.themeStateService.setSelectedTheme(this.form.value.theme);
} }
async saveMinOnCopyToClipboard() { async saveMinOnCopyToClipboard() {

View File

@ -577,7 +577,6 @@ export class AppComponent implements OnInit, OnDestroy {
await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
await this.cryptoService.clearKeys(userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut);
await this.settingsService.clear(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut);
await this.folderService.clear(userBeingLoggedOut); await this.folderService.clear(userBeingLoggedOut);
await this.collectionService.clear(userBeingLoggedOut); await this.collectionService.clear(userBeingLoggedOut);

View File

@ -1,15 +0,0 @@
import { Injectable } from "@angular/core";
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
@Injectable()
export class DesktopThemingService extends ThemingService {
protected async getSystemTheme(): Promise<ThemeType> {
return await ipc.platform.getSystemTheme();
}
protected monitorSystemThemeChanges(): void {
ipc.platform.onSystemThemeUpdated((theme: ThemeType) => this.updateSystemTheme(theme));
}
}

View File

@ -1,3 +1,4 @@
import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core"; import { Inject, Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
@ -38,6 +39,7 @@ export class InitService {
private themingService: AbstractThemingService, private themingService: AbstractThemingService,
private encryptService: EncryptService, private encryptService: EncryptService,
private configService: ConfigService, private configService: ConfigService,
@Inject(DOCUMENT) private document: Document,
) {} ) {}
init() { init() {
@ -52,14 +54,13 @@ export class InitService {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(true); this.syncService.fullSync(true);
await this.vaultTimeoutService.init(true); await this.vaultTimeoutService.init(true);
const locale = await this.stateService.getLocale(); await (this.i18nService as I18nRendererService).init();
await (this.i18nService as I18nRendererService).init(locale);
(this.eventUploadService as EventUploadService).init(true); (this.eventUploadService as EventUploadService).init(true);
this.twoFactorService.init(); this.twoFactorService.init();
setTimeout(() => this.notificationsService.init(), 3000); setTimeout(() => this.notificationsService.init(), 3000);
const htmlEl = this.win.document.documentElement; const htmlEl = this.win.document.documentElement;
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString()); htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
await this.themingService.monitorThemeChanges(); this.themingService.applyThemeChangesTo(this.document);
let installAction = null; let installAction = null;
const installedVersion = await this.stateService.getInstalledVersion(); const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion(); const currentVersion = await this.platformUtilsService.getApplicationVersion();

View File

@ -1,6 +1,5 @@
import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core"; import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { import {
SECURE_STORAGE, SECURE_STORAGE,
STATE_FACTORY, STATE_FACTORY,
@ -11,6 +10,7 @@ import {
OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_STORAGE,
WINDOW, WINDOW,
SYSTEM_THEME_OBSERVABLE,
} from "@bitwarden/angular/services/injection-tokens"; } from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -43,7 +43,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { StateProvider } from "@bitwarden/common/platform/state"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@ -60,13 +60,13 @@ import { ElectronRendererSecureStorageService } from "../../platform/services/el
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service"; import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
import { ElectronStateService } from "../../platform/services/electron-state.service"; import { ElectronStateService } from "../../platform/services/electron-state.service";
import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
import { NativeMessagingService } from "../../services/native-messaging.service"; import { NativeMessagingService } from "../../services/native-messaging.service";
import { SearchBarService } from "../layout/search/search-bar.service"; import { SearchBarService } from "../layout/search/search-bar.service";
import { DesktopFileDownloadService } from "./desktop-file-download.service"; import { DesktopFileDownloadService } from "./desktop-file-download.service";
import { DesktopThemingService } from "./desktop-theming.service";
import { InitService } from "./init.service"; import { InitService } from "./init.service";
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
@ -104,7 +104,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
{ {
provide: I18nServiceAbstraction, provide: I18nServiceAbstraction,
useClass: I18nRendererService, useClass: I18nRendererService,
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY], deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
}, },
{ {
provide: MessagingServiceAbstraction, provide: MessagingServiceAbstraction,
@ -149,8 +149,8 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
useClass: DesktopFileDownloadService, useClass: DesktopFileDownloadService,
}, },
{ {
provide: AbstractThemingService, provide: SYSTEM_THEME_OBSERVABLE,
useClass: DesktopThemingService, useFactory: () => fromIpcSystemTheme(),
}, },
{ {
provide: EncryptedMessageHandlerService, provide: EncryptedMessageHandlerService,

View File

@ -404,7 +404,7 @@
"message": "길이" "message": "길이"
}, },
"passwordMinLength": { "passwordMinLength": {
"message": "Minimum password length" "message": "최소 비밀번호 길이"
}, },
"uppercase": { "uppercase": {
"message": "대문자 (A-Z)" "message": "대문자 (A-Z)"
@ -561,10 +561,10 @@
"message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다."
}, },
"youSuccessfullyLoggedIn": { "youSuccessfullyLoggedIn": {
"message": "You successfully logged in" "message": "로그인에 성공했습니다."
}, },
"youMayCloseThisWindow": { "youMayCloseThisWindow": {
"message": "You may close this window" "message": "이제 창을 닫으실 수 있습니다."
}, },
"masterPassSent": { "masterPassSent": {
"message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다."
@ -780,7 +780,7 @@
"message": "문의하기" "message": "문의하기"
}, },
"helpAndFeedback": { "helpAndFeedback": {
"message": "Help and feedback" "message": "도움말 및 피드백"
}, },
"getHelp": { "getHelp": {
"message": "도움말" "message": "도움말"
@ -1399,7 +1399,7 @@
"message": "잘못된 PIN 코드입니다." "message": "잘못된 PIN 코드입니다."
}, },
"tooManyInvalidPinEntryAttemptsLoggingOut": { "tooManyInvalidPinEntryAttemptsLoggingOut": {
"message": "Too many invalid PIN entry attempts. Logging out." "message": "잘못된 PIN 입력 시도가 너무 많습니다. 로그아웃 합니다."
}, },
"unlockWithWindowsHello": { "unlockWithWindowsHello": {
"message": "Windows Hello를 사용하여 잠금 해제" "message": "Windows Hello를 사용하여 잠금 해제"
@ -1889,7 +1889,7 @@
"message": "Verification required for this action. Set a PIN to continue." "message": "Verification required for this action. Set a PIN to continue."
}, },
"setPin": { "setPin": {
"message": "Set PIN" "message": "PIN 설정"
}, },
"verifyWithBiometrics": { "verifyWithBiometrics": {
"message": "Verify with biometrics" "message": "Verify with biometrics"

View File

@ -97,7 +97,6 @@ export class Main {
} }
this.logService = new ElectronLogMainService(null, app.getPath("userData")); this.logService = new ElectronLogMainService(null, app.getPath("userData"));
this.i18nService = new I18nMainService("en", "./locales/");
const storageDefaults: any = {}; const storageDefaults: any = {};
// Default vault timeout to "on restart", and action to "lock" // Default vault timeout to "on restart", and action to "lock"
@ -112,6 +111,8 @@ export class Main {
); );
const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
const accountService = new AccountServiceImplementation( const accountService = new AccountServiceImplementation(
new NoopMessagingService(), new NoopMessagingService(),
this.logService, this.logService,
@ -218,8 +219,7 @@ export class Main {
this.migrationRunner.run().then( this.migrationRunner.run().then(
async () => { async () => {
await this.windowMain.init(); await this.windowMain.init();
const locale = await this.stateService.getLocale(); await this.i18nService.init();
await this.i18nService.init(locale != null ? locale : app.getLocale());
this.messagingMain.init(); this.messagingMain.init();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -246,8 +246,7 @@ export class WindowMain {
// Retrieve the background color // Retrieve the background color
// Resolves background color missmatch when starting the application. // Resolves background color missmatch when starting the application.
async getBackgroundColor(): Promise<string> { async getBackgroundColor(): Promise<string> {
const data: { theme?: string } = await this.storageService.get("global"); let theme = await this.storageService.get("global_theming_selection");
let theme = data?.theme;
if (theme == null || theme === "system") { if (theme == null || theme === "system") {
theme = nativeTheme.shouldUseDarkColors ? "dark" : "light"; theme = nativeTheme.shouldUseDarkColors ? "dark" : "light";

View File

@ -1,12 +1,12 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.2.2", "version": "2024.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.2.2", "version": "2024.3.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native" "@bitwarden/desktop-native": "file:../desktop_native"

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"productName": "Bitwarden", "productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.2.2", "version": "2024.3.1",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@ -1,14 +1,22 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { ipcMain } from "electron"; import { app, ipcMain } from "electron";
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export class I18nMainService extends BaseI18nService { export class I18nMainService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) { constructor(
super(systemLanguage, localesDirectory, (formattedLocale: string) => systemLanguage: string,
this.readLanguageFile(formattedLocale), localesDirectory: string,
globalStateProvider: GlobalStateProvider,
) {
super(
systemLanguage,
localesDirectory,
(formattedLocale: string) => this.readLanguageFile(formattedLocale),
globalStateProvider,
); );
ipcMain.handle("getLanguageFile", async (event, formattedLocale: string) => ipcMain.handle("getLanguageFile", async (event, formattedLocale: string) =>
@ -76,6 +84,12 @@ export class I18nMainService extends BaseI18nService {
]; ];
} }
override async init(): Promise<void> {
// Set system language to electron language
this.systemLanguage = app.getLocale();
await super.init();
}
private readLanguageFile(formattedLocale: string): Promise<any> { private readLanguageFile(formattedLocale: string): Promise<any> {
// Check that the provided locale only contains letters and dashes and underscores to avoid possible path traversal // Check that the provided locale only contains letters and dashes and underscores to avoid possible path traversal
if (!/^[a-zA-Z_-]+$/.test(formattedLocale)) { if (!/^[a-zA-Z_-]+$/.test(formattedLocale)) {

View File

@ -1,10 +1,20 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export class I18nRendererService extends BaseI18nService { export class I18nRendererService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) { constructor(
super(systemLanguage, localesDirectory, (formattedLocale: string) => { systemLanguage: string,
return ipc.platform.getLanguageFile(formattedLocale); localesDirectory: string,
}); globalStateProvider: GlobalStateProvider,
) {
super(
systemLanguage,
localesDirectory,
(formattedLocale: string) => {
return ipc.platform.getLanguageFile(formattedLocale);
},
globalStateProvider,
);
// Please leave 'en' where it is, as it's our fallback language in case no translation can be found // Please leave 'en' where it is, as it's our fallback language in case no translation can be found
this.supportedTranslationLocales = [ this.supportedTranslationLocales = [

View File

@ -0,0 +1,15 @@
import { defer, fromEventPattern, merge } from "rxjs";
import { ThemeType } from "@bitwarden/common/platform/enums";
/**
* @returns An observable watching the system theme via IPC channels
*/
export const fromIpcSystemTheme = () => {
return merge(
defer(() => ipc.platform.getSystemTheme()),
fromEventPattern<ThemeType>((handler) =>
ipc.platform.onSystemThemeUpdated((theme) => handler(theme)),
),
);
};

View File

@ -1,7 +1,6 @@
const path = require("path"); const path = require("path");
const { merge } = require("webpack-merge"); const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const configurator = require("./config/config"); const configurator = require("./config/config");
const { EnvironmentPlugin } = require("webpack"); const { EnvironmentPlugin } = require("webpack");

View File

@ -1,7 +1,6 @@
const path = require("path"); const path = require("path");
const { merge } = require("webpack-merge"); const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const configurator = require("./config/config"); const configurator = require("./config/config");
const { EnvironmentPlugin } = require("webpack"); const { EnvironmentPlugin } = require("webpack");

View File

@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/web-vault", "name": "@bitwarden/web-vault",
"version": "2024.2.5", "version": "2024.3.0",
"scripts": { "scripts": {
"build:oss": "webpack", "build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@ -16,7 +16,10 @@
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex"> <bit-tab-group
*ngIf="!loading && organization$ | async as organization"
[(selectedIndex)]="tabIndex"
>
<bit-tab [label]="'role' | i18n"> <bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode"> <ng-container *ngIf="!editMode">
<p>{{ "inviteUserDesc" | i18n }}</p> <p>{{ "inviteUserDesc" | i18n }}</p>
@ -60,7 +63,10 @@
</div> </div>
</label> </label>
</div> </div>
<div *ngIf="!flexibleCollectionsEnabled" class="tw-mb-2 tw-flex tw-items-baseline"> <div
*ngIf="!organization.flexibleCollections"
class="tw-mb-2 tw-flex tw-items-baseline"
>
<input <input
type="radio" type="radio"
id="userTypeManager" id="userTypeManager"
@ -116,11 +122,11 @@
formControlName="type" formControlName="type"
name="type" name="type"
class="tw-relative tw-bottom-[-1px] tw-mr-2" class="tw-relative tw-bottom-[-1px] tw-mr-2"
[attr.disabled]="!canUseCustomPermissions || null" [attr.disabled]="!organization.useCustomPermissions || null"
/> />
<label class="tw-m-0" for="userTypeCustom"> <label class="tw-m-0" for="userTypeCustom">
{{ "custom" | i18n }} {{ "custom" | i18n }}
<ng-container *ngIf="!canUseCustomPermissions; else enterprise"> <ng-container *ngIf="!organization.useCustomPermissions; else enterprise">
<div class="text-base tw-block tw-font-normal tw-text-muted"> <div class="text-base tw-block tw-font-normal tw-text-muted">
{{ "customDescNonEnterpriseStart" | i18n {{ "customDescNonEnterpriseStart" | i18n
}}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{ }}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{
@ -138,7 +144,7 @@
</div> </div>
</fieldset> </fieldset>
<ng-container *ngIf="customUserTypeSelected"> <ng-container *ngIf="customUserTypeSelected">
<ng-container *ngIf="!flexibleCollectionsEnabled; else customPermissionsFC"> <ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
<h3 class="mt-4 d-flex tw-font-semibold"> <h3 class="mt-4 d-flex tw-font-semibold">
{{ "permissions" | i18n }} {{ "permissions" | i18n }}
</h3> </h3>
@ -365,7 +371,7 @@
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngIf="canUseSecretsManager"> <ng-container *ngIf="organization.useSecretsManager">
<h3 class="mt-4"> <h3 class="mt-4">
{{ "secretsManager" | i18n }} {{ "secretsManager" | i18n }}
<a <a
@ -401,14 +407,14 @@
[columnHeader]="'groups' | i18n" [columnHeader]="'groups' | i18n"
[selectorLabelText]="'selectGroups' | i18n" [selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n" [emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled" [flexibleCollectionsEnabled]="organization.flexibleCollections"
></bit-access-selector> ></bit-access-selector>
</bit-tab> </bit-tab>
<bit-tab [label]="'collections' | i18n"> <bit-tab [label]="'collections' | i18n">
<div *ngIf="organization.useGroups" class="tw-mb-6"> <div *ngIf="organization.useGroups" class="tw-mb-6">
{{ "userPermissionOverrideHelper" | i18n }} {{ "userPermissionOverrideHelper" | i18n }}
</div> </div>
<div *ngIf="!flexibleCollectionsEnabled" class="tw-mb-6"> <div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
<bit-form-control> <bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessAllCollections" /> <input type="checkbox" bitCheckbox formControlName="accessAllCollections" />
<bit-label> <bit-label>
@ -434,7 +440,7 @@
[columnHeader]="'collection' | i18n" [columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n" [selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled" [flexibleCollectionsEnabled]="organization.flexibleCollections"
></bit-access-selector ></bit-access-selector
></bit-tab> ></bit-tab>
</bit-tab-group> </bit-tab-group>

View File

@ -1,7 +1,16 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs"; import {
combineLatest,
firstValueFrom,
Observable,
of,
shareReplay,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@ -18,7 +27,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { flagEnabled } from "../../../../../../utils/flags";
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
import { import {
CollectionAccessSelectionView, CollectionAccessSelectionView,
@ -66,7 +74,7 @@ export enum MemberDialogResult {
@Component({ @Component({
templateUrl: "member-dialog.component.html", templateUrl: "member-dialog.component.html",
}) })
export class MemberDialogComponent implements OnInit, OnDestroy { export class MemberDialogComponent implements OnDestroy {
loading = true; loading = true;
editMode = false; editMode = false;
isRevoked = false; isRevoked = false;
@ -74,12 +82,10 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
access: "all" | "selected" = "selected"; access: "all" | "selected" = "selected";
collections: CollectionView[] = []; collections: CollectionView[] = [];
organizationUserType = OrganizationUserType; organizationUserType = OrganizationUserType;
canUseCustomPermissions: boolean;
PermissionMode = PermissionMode; PermissionMode = PermissionMode;
canUseSecretsManager: boolean;
showNoMasterPasswordWarning = false; showNoMasterPasswordWarning = false;
protected organization: Organization; protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = []; protected collectionAccessItems: AccessItemView[] = [];
protected groupAccessItems: AccessItemView[] = []; protected groupAccessItems: AccessItemView[] = [];
protected tabIndex: MemberDialogTab; protected tabIndex: MemberDialogTab;
@ -130,7 +136,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
private dialogRef: DialogRef<MemberDialogResult>, private dialogRef: DialogRef<MemberDialogResult>,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
// TODO: We should really look into consolidating naming conventions for these services // TODO: We should really look into consolidating naming conventions for these services
private collectionAdminService: CollectionAdminService, private collectionAdminService: CollectionAdminService,
@ -139,28 +144,26 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
private organizationUserService: OrganizationUserService, private organizationUserService: OrganizationUserService,
private dialogService: DialogService, private dialogService: DialogService,
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
) {} organizationService: OrganizationService,
) {
this.organization$ = organizationService
.get$(this.params.organizationId)
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
async ngOnInit() {
this.editMode = this.params.organizationUserId != null; this.editMode = this.params.organizationUserId != null;
this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role;
this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember");
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe( const groups$ = this.organization$.pipe(
shareReplay({ refCount: true, bufferSize: 1 }), switchMap((organization) =>
); organization.useGroups
const groups$ = organization$.pipe( ? this.groupService.getAll(this.params.organizationId)
switchMap((organization) => { : of([] as GroupView[]),
if (!organization.useGroups) { ),
return of([] as GroupView[]);
}
return this.groupService.getAll(this.params.organizationId);
}),
); );
combineLatest({ combineLatest({
organization: organization$, organization: this.organization$,
collections: this.collectionAdminService.getAll(this.params.organizationId), collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: this.params.organizationUserId userDetails: this.params.organizationUserId
? this.userService.get(this.params.organizationId, this.params.organizationUserId) ? this.userService.get(this.params.organizationId, this.params.organizationUserId)
@ -169,23 +172,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(({ organization, collections, userDetails, groups }) => { .subscribe(({ organization, collections, userDetails, groups }) => {
this.organization = organization; this.setFormValidators(organization);
this.canUseCustomPermissions = organization.useCustomPermissions;
this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager");
const emailsControlValidators = [
Validators.required,
commaSeparatedEmails,
orgSeatLimitReachedValidator(
this.organization,
this.params.allOrganizationUserEmails,
this.i18nService.t("subscriptionUpgrade", organization.seats),
),
];
const emailsControl = this.formGroup.get("emails");
emailsControl.setValidators(emailsControlValidators);
emailsControl.updateValueAndValidity();
this.collectionAccessItems = [].concat( this.collectionAccessItems = [].concat(
collections.map((c) => mapCollectionToAccessItemView(c)), collections.map((c) => mapCollectionToAccessItemView(c)),
@ -196,77 +183,101 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
); );
if (this.params.organizationUserId) { if (this.params.organizationUserId) {
if (!userDetails) { this.loadOrganizationUser(userDetails, groups, collections);
throw new Error("Could not find user to edit.");
}
this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked;
this.showNoMasterPasswordWarning =
userDetails.status > OrganizationUserStatusType.Invited &&
userDetails.hasMasterPassword === false;
const assignedCollectionsPermissions = {
editAssignedCollections: userDetails.permissions.editAssignedCollections,
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
manageAssignedCollections:
userDetails.permissions.editAssignedCollections &&
userDetails.permissions.deleteAssignedCollections,
};
const allCollectionsPermissions = {
createNewCollections: userDetails.permissions.createNewCollections,
editAnyCollection: userDetails.permissions.editAnyCollection,
deleteAnyCollection: userDetails.permissions.deleteAnyCollection,
manageAllCollections:
userDetails.permissions.createNewCollections &&
userDetails.permissions.editAnyCollection &&
userDetails.permissions.deleteAnyCollection,
};
if (userDetails.type === OrganizationUserType.Custom) {
this.permissionsGroup.patchValue({
accessEventLogs: userDetails.permissions.accessEventLogs,
accessImportExport: userDetails.permissions.accessImportExport,
accessReports: userDetails.permissions.accessReports,
manageGroups: userDetails.permissions.manageGroups,
manageSso: userDetails.permissions.manageSso,
managePolicies: userDetails.permissions.managePolicies,
manageUsers: userDetails.permissions.manageUsers,
manageResetPassword: userDetails.permissions.manageResetPassword,
manageAssignedCollectionsGroup: assignedCollectionsPermissions,
manageAllCollectionsGroup: allCollectionsPermissions,
});
}
const collectionsFromGroups = groups
.filter((group) => userDetails.groups.includes(group.id))
.flatMap((group) =>
group.collections.map((accessSelection) => {
const collection = collections.find((c) => c.id === accessSelection.id);
return { group, collection, accessSelection };
}),
);
this.collectionAccessItems = this.collectionAccessItems.concat(
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
mapCollectionToAccessItemView(collection, accessSelection, group),
),
);
const accessSelections = mapToAccessSelections(userDetails);
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
this.formGroup.removeControl("emails");
this.formGroup.patchValue({
type: userDetails.type,
externalId: userDetails.externalId,
accessAllCollections: userDetails.accessAll,
access: accessSelections,
accessSecretsManager: userDetails.accessSecretsManager,
groups: groupAccessSelections,
});
} }
this.loading = false; this.loading = false;
}); });
} }
private setFormValidators(organization: Organization) {
const emailsControlValidators = [
Validators.required,
commaSeparatedEmails,
orgSeatLimitReachedValidator(
organization,
this.params.allOrganizationUserEmails,
this.i18nService.t("subscriptionUpgrade", organization.seats),
),
];
const emailsControl = this.formGroup.get("emails");
emailsControl.setValidators(emailsControlValidators);
emailsControl.updateValueAndValidity();
}
private loadOrganizationUser(
userDetails: OrganizationUserAdminView,
groups: GroupView[],
collections: CollectionView[],
) {
if (!userDetails) {
throw new Error("Could not find user to edit.");
}
this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked;
this.showNoMasterPasswordWarning =
userDetails.status > OrganizationUserStatusType.Invited &&
userDetails.hasMasterPassword === false;
const assignedCollectionsPermissions = {
editAssignedCollections: userDetails.permissions.editAssignedCollections,
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
manageAssignedCollections:
userDetails.permissions.editAssignedCollections &&
userDetails.permissions.deleteAssignedCollections,
};
const allCollectionsPermissions = {
createNewCollections: userDetails.permissions.createNewCollections,
editAnyCollection: userDetails.permissions.editAnyCollection,
deleteAnyCollection: userDetails.permissions.deleteAnyCollection,
manageAllCollections:
userDetails.permissions.createNewCollections &&
userDetails.permissions.editAnyCollection &&
userDetails.permissions.deleteAnyCollection,
};
if (userDetails.type === OrganizationUserType.Custom) {
this.permissionsGroup.patchValue({
accessEventLogs: userDetails.permissions.accessEventLogs,
accessImportExport: userDetails.permissions.accessImportExport,
accessReports: userDetails.permissions.accessReports,
manageGroups: userDetails.permissions.manageGroups,
manageSso: userDetails.permissions.manageSso,
managePolicies: userDetails.permissions.managePolicies,
manageUsers: userDetails.permissions.manageUsers,
manageResetPassword: userDetails.permissions.manageResetPassword,
manageAssignedCollectionsGroup: assignedCollectionsPermissions,
manageAllCollectionsGroup: allCollectionsPermissions,
});
}
const collectionsFromGroups = groups
.filter((group) => userDetails.groups.includes(group.id))
.flatMap((group) =>
group.collections.map((accessSelection) => {
const collection = collections.find((c) => c.id === accessSelection.id);
return { group, collection, accessSelection };
}),
);
this.collectionAccessItems = this.collectionAccessItems.concat(
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
mapCollectionToAccessItemView(collection, accessSelection, group),
),
);
const accessSelections = mapToAccessSelections(userDetails);
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
this.formGroup.removeControl("emails");
this.formGroup.patchValue({
type: userDetails.type,
externalId: userDetails.externalId,
accessAllCollections: userDetails.accessAll,
access: accessSelections,
accessSecretsManager: userDetails.accessSecretsManager,
groups: groupAccessSelections,
});
}
check(c: CollectionView, select?: boolean) { check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select; (c as any).checked = select == null ? !(c as any).checked : select;
if (!(c as any).checked) { if (!(c as any).checked) {
@ -335,7 +346,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return; return;
} }
if (!this.canUseCustomPermissions && this.customUserTypeSelected) { const organization = await firstValueFrom(this.organization$);
if (!organization.useCustomPermissions && this.customUserTypeSelected) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",
null, null,
@ -363,8 +376,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
await this.userService.save(userView); await this.userService.save(userView);
} else { } else {
userView.id = this.params.organizationUserId; userView.id = this.params.organizationUserId;
const maxEmailsCount = const maxEmailsCount = organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
this.organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
if (emails.length > maxEmailsCount) { if (emails.length > maxEmailsCount) {
this.formGroup.controls.emails.setErrors({ this.formGroup.controls.emails.setErrors({
@ -373,8 +385,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return; return;
} }
if ( if (
this.organization.hasReseller && organization.hasReseller &&
this.params.numConfirmedMembers + emails.length > this.organization.seats this.params.numConfirmedMembers + emails.length > organization.seats
) { ) {
this.formGroup.controls.emails.setErrors({ this.formGroup.controls.emails.setErrors({
tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") }, tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") },
@ -515,10 +527,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
}); });
} }
protected get flexibleCollectionsEnabled() {
return this.organization?.flexibleCollections;
}
protected readonly ProductType = ProductType; protected readonly ProductType = ProductType;
} }

View File

@ -275,7 +275,6 @@ export class AppComponent implements OnDestroy, OnInit {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),

View File

@ -23,14 +23,20 @@ import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/comm
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */ /* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
import {
DefaultThemeStateService,
ThemeStateService,
} from "@bitwarden/common/platform/theming/theme-state.service";
import { PolicyListService } from "../admin-console/core/policy-list.service"; import { PolicyListService } from "../admin-console/core/policy-list.service";
import { HtmlStorageService } from "../core/html-storage.service"; import { HtmlStorageService } from "../core/html-storage.service";
@ -74,7 +80,7 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
{ {
provide: I18nServiceAbstraction, provide: I18nServiceAbstraction,
useClass: I18nService, useClass: I18nService,
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY], deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
}, },
{ provide: AbstractStorageService, useClass: HtmlStorageService }, { provide: AbstractStorageService, useClass: HtmlStorageService },
{ {
@ -132,6 +138,13 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
OBSERVABLE_DISK_LOCAL_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE,
], ],
}, },
{
provide: ThemeStateService,
useFactory: (globalStateProvider: GlobalStateProvider) =>
// Web chooses to have Light as the default theme
new DefaultThemeStateService(globalStateProvider, ThemeType.Light),
deps: [GlobalStateProvider],
},
], ],
}) })
export class CoreModule { export class CoreModule {

View File

@ -1,20 +1,30 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { SupportedTranslationLocales } from "../../translation-constants"; import { SupportedTranslationLocales } from "../../translation-constants";
export class I18nService extends BaseI18nService { export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) { constructor(
super(systemLanguage || "en-US", localesDirectory, async (formattedLocale: string) => { systemLanguage: string,
const filePath = localesDirectory: string,
this.localesDirectory + globalStateProvider: GlobalStateProvider,
"/" + ) {
formattedLocale + super(
"/messages.json?cache=" + systemLanguage || "en-US",
process.env.CACHE_TAG; localesDirectory,
const localesResult = await fetch(filePath); async (formattedLocale: string) => {
const locales = await localesResult.json(); const filePath =
return locales; this.localesDirectory +
}); "/" +
formattedLocale +
"/messages.json?cache=" +
process.env.CACHE_TAG;
const localesResult = await fetch(filePath);
const locales = await localesResult.json();
return locales;
},
globalStateProvider,
);
this.supportedTranslationLocales = SupportedTranslationLocales; this.supportedTranslationLocales = SupportedTranslationLocales;
} }

View File

@ -1,3 +1,4 @@
import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core"; import { Inject, Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
@ -18,8 +19,6 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
import { I18nService } from "../core/i18n.service";
@Injectable() @Injectable()
export class InitService { export class InitService {
constructor( constructor(
@ -35,6 +34,7 @@ export class InitService {
private themingService: AbstractThemingService, private themingService: AbstractThemingService,
private encryptService: EncryptService, private encryptService: EncryptService,
private configService: ConfigService, private configService: ConfigService,
@Inject(DOCUMENT) private document: Document,
) {} ) {}
init() { init() {
@ -50,13 +50,12 @@ export class InitService {
setTimeout(() => this.notificationsService.init(), 3000); setTimeout(() => this.notificationsService.init(), 3000);
await this.vaultTimeoutService.init(true); await this.vaultTimeoutService.init(true);
const locale = await this.stateService.getLocale(); await this.i18nService.init();
await (this.i18nService as I18nService).init(locale);
(this.eventUploadService as EventUploadService).init(true); (this.eventUploadService as EventUploadService).init(true);
this.twoFactorService.init(); this.twoFactorService.init();
const htmlEl = this.win.document.documentElement; const htmlEl = this.win.document.documentElement;
htmlEl.classList.add("locale_" + this.i18nService.translationLocale); htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
await this.themingService.monitorThemeChanges(); this.themingService.applyThemeChangesTo(this.document);
const containerService = new ContainerService(this.cryptoService, this.encryptService); const containerService = new ContainerService(this.cryptoService, this.encryptService);
containerService.attachToGlobal(this.win); containerService.attachToGlobal(this.win);

View File

@ -1,7 +1,5 @@
import { ThemeType } from "@bitwarden/common/platform/enums";
import { GlobalState as BaseGlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { GlobalState as BaseGlobalState } from "@bitwarden/common/platform/models/domain/global-state";
export class GlobalState extends BaseGlobalState { export class GlobalState extends BaseGlobalState {
theme?: ThemeType = ThemeType.Light;
rememberEmail = true; rememberEmail = true;
} }

View File

@ -1,16 +1,24 @@
import { APP_INITIALIZER, NgModule } from "@angular/core"; import { APP_INITIALIZER, NgModule } from "@angular/core";
import { Observable, of } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; import { TranslationService } from "@bitwarden/common/platform/services/translation.service";
import eng from "../../../locales/en/messages.json"; import eng from "../../../locales/en/messages.json";
class PreloadedEnglishI18nService extends BaseI18nService { class PreloadedEnglishI18nService extends TranslationService implements I18nService {
translationLocale = "en";
userSetLocale$: Observable<string | undefined> = of("en");
locale$: Observable<string> = of("en");
constructor() { constructor() {
super("en", "", () => { super("en", "", () => {
return Promise.resolve(eng); return Promise.resolve(eng);
}); });
} }
setLocale(): Promise<void> {
throw new Error("Method not implemented.");
}
} }
function i18nInitializer(i18nService: I18nService): () => Promise<void> { function i18nInitializer(i18nService: I18nService): () => Promise<void> {

View File

@ -4,8 +4,8 @@
[ariaLabel]="['organization' | i18n, activeOrganization.name].join(' ')" [ariaLabel]="['organization' | i18n, activeOrganization.name].join(' ')"
icon="bwi-business" icon="bwi-business"
[route]="['../', activeOrganization.id]" [route]="['../', activeOrganization.id]"
[routerLinkActiveOptions]="{ exact: true }"
[(open)]="open" [(open)]="open"
[exactMatch]="true"
> >
<i <i
slot="end" slot="end"
@ -21,7 +21,7 @@
[ariaLabel]="['organization' | i18n, org.name].join(' ')" [ariaLabel]="['organization' | i18n, org.name].join(' ')"
[route]="['../', org.id]" [route]="['../', org.id]"
(mainContentClicked)="toggle()" (mainContentClicked)="toggle()"
[exactMatch]="true" [routerLinkActiveOptions]="{ exact: true }"
> >
<i <i
slot="end" slot="end"

View File

@ -2,7 +2,6 @@ import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs"; import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -14,6 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@Component({ @Component({
@ -35,7 +35,6 @@ export class PreferencesComponent implements OnInit {
themeOptions: any[]; themeOptions: any[];
private startingLocale: string; private startingLocale: string;
private startingTheme: ThemeType;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
form = this.formBuilder.group({ form = this.formBuilder.group({
@ -54,7 +53,7 @@ export class PreferencesComponent implements OnInit {
private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService, private messagingService: MessagingService,
private themingService: AbstractThemingService, private themeStateService: ThemeStateService,
private settingsService: SettingsService, private settingsService: SettingsService,
private dialogService: DialogService, private dialogService: DialogService,
) { ) {
@ -141,11 +140,10 @@ export class PreferencesComponent implements OnInit {
this.vaultTimeoutSettingsService.vaultTimeoutAction$(), this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
), ),
enableFavicons: !(await this.settingsService.getDisableFavicon()), enableFavicons: !(await this.settingsService.getDisableFavicon()),
theme: await this.stateService.getTheme(), theme: await firstValueFrom(this.themeStateService.selectedTheme$),
locale: (await this.stateService.getLocale()) ?? null, locale: (await firstValueFrom(this.i18nService.userSetLocale$)) ?? null,
}; };
this.startingLocale = initialFormValues.locale; this.startingLocale = initialFormValues.locale;
this.startingTheme = initialFormValues.theme;
this.form.setValue(initialFormValues, { emitEvent: false }); this.form.setValue(initialFormValues, { emitEvent: false });
} }
@ -165,11 +163,8 @@ export class PreferencesComponent implements OnInit {
values.vaultTimeoutAction, values.vaultTimeoutAction,
); );
await this.settingsService.setDisableFavicon(!values.enableFavicons); await this.settingsService.setDisableFavicon(!values.enableFavicons);
if (values.theme !== this.startingTheme) { await this.themeStateService.setSelectedTheme(values.theme);
await this.themingService.updateConfiguredTheme(values.theme); await this.i18nService.setLocale(values.locale);
this.startingTheme = values.theme;
}
await this.stateService.setLocale(values.locale);
if (values.locale !== this.startingLocale) { if (values.locale !== this.startingLocale) {
window.location.reload(); window.location.reload();
} else { } else {

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Toegang" "message": "Toegang"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Uitgeteken" "message": "Uitgeteken"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "وصول" "message": "وصول"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "تم تسجيل الخروج" "message": "تم تسجيل الخروج"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Müraciət" "message": "Müraciət"
}, },
"accessLevel": {
"message": "Müraciət səviyyəsi"
},
"loggedOut": { "loggedOut": {
"message": ıxış edildi" "message": ıxış edildi"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Доступ" "message": "Доступ"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Вы выйшлі" "message": "Вы выйшлі"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Достъп" "message": "Достъп"
}, },
"accessLevel": {
"message": "Ниво на достъп"
},
"loggedOut": { "loggedOut": {
"message": "Бяхте отписани" "message": "Бяхте отписани"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Pristup" "message": "Pristup"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Accés" "message": "Accés"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Sessió tancada" "message": "Sessió tancada"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Přístup" "message": "Přístup"
}, },
"accessLevel": {
"message": "Úroveň přístupu"
},
"loggedOut": { "loggedOut": {
"message": "Odhlášení" "message": "Odhlášení"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Adgang" "message": "Adgang"
}, },
"accessLevel": {
"message": "Adgangsniveau"
},
"loggedOut": { "loggedOut": {
"message": "Logget ud" "message": "Logget ud"
}, },
@ -669,7 +672,7 @@
"message": "Kryptering ikke understøttet" "message": "Kryptering ikke understøttet"
}, },
"enablePasskeyEncryption": { "enablePasskeyEncryption": {
"message": "Set up encryption" "message": "Opsæt kryptering"
}, },
"usedForEncryption": { "usedForEncryption": {
"message": "Bruges til kryptering" "message": "Bruges til kryptering"
@ -5937,10 +5940,10 @@
"message": "Start DUO og følg trinene for at fuldføre indlogningen." "message": "Start DUO og følg trinene for at fuldføre indlogningen."
}, },
"duoRequiredByOrgForAccount": { "duoRequiredByOrgForAccount": {
"message": "DUO totrins indlogning kræves for kontoen." "message": "Duo totrinsindlogning kræves for kontoen."
}, },
"launchDuo": { "launchDuo": {
"message": "Start DUO" "message": "Start Duo"
}, },
"turnOn": { "turnOn": {
"message": "Slå til" "message": "Slå til"
@ -6731,7 +6734,7 @@
} }
}, },
"teamsStarterPlanInvLimitReachedManageBilling": { "teamsStarterPlanInvLimitReachedManageBilling": {
"message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", "message": "Teams Starter kan have op til $SEATCOUNT$ medlemmer. Opgradér til en betalt abonnementstype for at invitere flere deltagerer.",
"placeholders": { "placeholders": {
"seatcount": { "seatcount": {
"content": "$1", "content": "$1",
@ -7474,7 +7477,7 @@
} }
}, },
"seeDetailedInstructions": { "seeDetailedInstructions": {
"message": "See detailed instructions on our help site at", "message": "Se detaljeret vejledning på vores hjælpewebsted på",
"description": "This is followed a by a hyperlink to the help website." "description": "This is followed a by a hyperlink to the help website."
}, },
"installBrowserExtension": { "installBrowserExtension": {

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Zugriff" "message": "Zugriff"
}, },
"accessLevel": {
"message": "Zugriffsebene"
},
"loggedOut": { "loggedOut": {
"message": "Ausgeloggt" "message": "Ausgeloggt"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Πρόσβαση" "message": "Πρόσβαση"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Αποσυνδεθήκατε" "message": "Αποσυνδεθήκατε"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Adiaŭita" "message": "Adiaŭita"
}, },

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