diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 01361a97404..74c4ceed948 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -175,6 +175,7 @@ "del", "ed25519", "lit", + "@lit-labs/signals", "patch-package", "pkcs8", "prettier", diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 48ecca540e8..72b60da97a1 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -33,6 +33,10 @@ on: description: "Custom SDK branch" required: false type: string + testflight_distribute: + description: "Force distribute to TestFlight regardless of branch (useful for QA testing on feature branches)" + type: boolean + default: true defaults: run: @@ -1208,21 +1212,45 @@ jobs: path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg if-no-files-found: error + - name: Create secrets for Fastlane + if: | + github.event_name != 'pull_request_target' + && (inputs.testflight_distribute || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') + run: | + brew install gsed + + KEY_WITHOUT_NEWLINES=$(gsed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' ~/private_keys/AuthKey_6TV9MKN3GP.p8) + + cat << EOF > ~/secrets/appstoreconnect-fastlane.json + { + "issuer_id": "${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}", + "key_id": "6TV9MKN3GP", + "key": "$KEY_WITHOUT_NEWLINES" + } + EOF + - name: Deploy to TestFlight id: testflight-deploy if: | github.event_name != 'pull_request_target' - && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') + && (inputs.testflight_distribute || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') env: APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP + BRANCH: ${{ github.ref }} run: | - xcrun altool \ - --upload-app \ - --type macos \ - --file "$(find ./dist/mas-universal/Bitwarden*.pkg)" \ - --apiKey $APP_STORE_CONNECT_AUTH_KEY \ - --apiIssuer $APP_STORE_CONNECT_TEAM_ISSUER + + GIT_CHANGE="$(git show -s --format=%s)" + + BRANCH=$(echo $BRANCH | sed 's/refs\/heads\///') + + CHANGELOG="$BRANCH: $GIT_CHANGE" + + fastlane pilot upload \ + --app_identifier "com.bitwarden.desktop" \ + --changelog "$CHANGELOG" \ + --api_key_path $HOME/secrets/appstoreconnect-fastlane.json \ + --pkg "$(find ./dist/mas-universal/Bitwarden*.pkg)" - name: Post message to a Slack channel id: slack-message diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b039315b30..6411337f6e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,8 +71,6 @@ jobs: - name: Upload results to codecov.io uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} rust: name: Run Rust tests on ${{ matrix.os }} diff --git a/README.md b/README.md index 22c8d329f1c..cdeaa4c8cf7 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ # Bitwarden Client Applications -This repository houses all Bitwarden client applications except the [Mobile application](https://github.com/bitwarden/mobile). +This repository houses all Bitwarden client applications except the mobile applications ([iOS](https://github.com/bitwarden/ios) | [android](https://github.com/bitwarden/android)). Please refer to the [Clients section](https://contributing.bitwarden.com/getting-started/clients/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. ## Related projects: - [bitwarden/server](https://github.com/bitwarden/server): The core infrastructure backend (API, database, Docker, etc). -- [bitwarden/ios](https://github.com/bitwarden/ios): Bitwarden mobile app for iOS. -- [bitwarden/android](https://github.com/bitwarden/android): Bitwarden mobile app for Android. +- [bitwarden/ios](https://github.com/bitwarden/ios): Bitwarden iOS Password Manager & Authenticator apps. +- [bitwarden/android](https://github.com/bitwarden/android): Bitwarden Android Password Manager & Authenticator apps. - [bitwarden/directory-connector](https://github.com/bitwarden/directory-connector): A tool for syncing a directory (AD, LDAP, Azure, G Suite, Okta) to an organization. # We're Hiring! diff --git a/apps/browser/package.json b/apps/browser/package.json index 5a8ddd03b41..6ef35d88c10 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.3.1", + "version": "2025.3.2", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 4c051d455ac..bf22a38be08 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6f3ace453ae..1ca7642eb19 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Yalnız $EMAIL$ ilə əlaqələndirilmiş qoşmalar daxil olmaqla fərdi anbar elementləri xaricə köçürüləcək. Təşkilat anbar elementləri daxil edilməyəcək", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Təşkilat seyfini xaricə köçürmə" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Avto-doldur - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopyala: $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 905f6b6ac1d..a9a237aeb99 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -23,7 +23,7 @@ "message": "Упершыню ў Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Увайсці з ключом доступу" }, "useSingleSignOn": { "message": "Выкарыстаць аднаразовы ўваход" @@ -35,7 +35,7 @@ "message": "Прызначыць надзейны пароль" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Завяршыць стварэнне вашага ўліковага запісу нарадзіўшы пароль" }, "enterpriseSingleSignOn": { "message": "Адзіны ўваход прадпрыемства (SSO)" @@ -62,7 +62,7 @@ "message": "Падказка да асноўнага пароля можа дапамагчы вам успомніць яго, калі вы яго забылі." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Падказка пароля можа быць адпраўлена на ваш адрас электроннай пошты, калі вы яго забудзеце. Максімум сімвалаў: $CURRENT$/$MAXIMUM$.", "placeholders": { "current": { "content": "$1", @@ -81,7 +81,7 @@ "message": "Падказка да асноўнага пароля (неабавязкова)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Ацэнка надзейнасці пароля $SCORE$", "placeholders": { "score": { "content": "$1", @@ -206,10 +206,10 @@ "message": "Аўтазапаўненне асабістых даных" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Запоўніць праверачны код" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Запоўніць праверачны код", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -261,7 +261,7 @@ "message": "Запытаць падказку да асноўнага пароля" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Увядзіце адрас электроннай пошты ўліковага запісу і падказка пароля будзе адпраўлена вам" }, "getMasterPasswordHint": { "message": "Атрымаць падказку да асноўнага пароля" @@ -291,7 +291,7 @@ "message": "Працягнуць у вэб-праграме?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Даследуйце больш функцый вашага уліковага запісу Bitwarden у вэб-праграме." }, "continueToHelpCenter": { "message": "Працягнуць працу ў Даведачным цэнтры?" @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Экспартаванне сховішча арганізацыі" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 226e63e32cb..c5dd0237a2c 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Ще бъдат изнесени само записите и прикачените файлове от личния трезор свързан с $EMAIL$. Записите в трезора на организацията няма да бъдат включени.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Изнасяне на трезора на организацията" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Авт. попълване – $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Копиране на $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 983b9fadde4..21a455265e4 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 08fedb6f10a..a2457c94080 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 14e4a577440..d16a679824d 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Només s'exportaran els elements personals incloent adjunts de la caixa forta associats a $EMAIL$. Els elements de la caixa forta de l'organització no s'inclouran", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "S'està exportant la caixa forta de l’organització" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index e106d371d57..e6bf4a728e4 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Budou exportovány jen osobní položky trezoru včetně příloh spojené s účtem $EMAIL$. Nebudou zahrnuty položky trezoru v organizaci.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exportování trezoru organizace" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Zobrazit položku - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Automatické vyplnění - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Automatické vyplnění - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopírovat $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 83d09d13273..5ccff5a8332 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 69c8b28d29a..f66f8f34495 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Eksport af organisationsboks" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autoudfyld - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index a7439db6432..25e8b53cdb5 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Tresor der Organisation wird exportiert" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Auto-Ausfüllen - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "$FIELD$, $VALUE$ kopieren", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index fc07f12a48f..47dca3701ec 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Εξαγωγή θησαυ/κίου οργανισμού" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Αυτόματη συμπλήρωση - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 16f462b7f17..8c47db0d331 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1391,24 +1391,12 @@ "premiumRequiredDesc": { "message": "A Premium membership is required to use this feature." }, - "enterVerificationCodeApp": { - "message": "Enter the 6 digit verification code from your authenticator app." - }, "authenticationTimeout": { "message": "Authentication timeout" }, "authenticationSessionTimedOut": { "message": "The authentication session timed out. Please restart the login process." }, - "enterVerificationCodeEmail": { - "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", - "placeholders": { - "email": { - "content": "$1", - "example": "example@gmail.com" - } - } - }, "verificationCodeEmailSent": { "message": "Verification email sent to $EMAIL$.", "placeholders": { @@ -1418,18 +1406,9 @@ } } }, - "rememberMe": { - "message": "Remember me" - }, "dontAskAgainOnThisDeviceFor30Days": { "message": "Don't ask again on this device for 30 days" }, - "sendVerificationCodeEmailAgain": { - "message": "Send verification code email again" - }, - "useAnotherTwoStepMethod": { - "message": "Use another two-step login method" - }, "selectAnotherMethod": { "message": "Select another method", "description": "Select another two-step login method" @@ -1437,18 +1416,9 @@ "useYourRecoveryCode": { "message": "Use your recovery code" }, - "insertYubiKey": { - "message": "Insert your YubiKey into your computer's USB port, then touch its button." - }, "insertU2f": { "message": "Insert your security key into your computer's USB port. If it has a button, touch it." }, - "webAuthnNewTab": { - "message": "To start the WebAuthn 2FA verification. Click the button below to open a new tab and follow the instructions provided in the new tab." - }, - "webAuthnNewTabOpen": { - "message": "Open new tab" - }, "openInNewTab": { "message": "Open in new tab" }, @@ -2534,15 +2504,15 @@ "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, - "reviewAtRiskLoginSlideImgAlt": { - "message": "Illustration of a list of logins that are at-risk" + "reviewAtRiskLoginSlideImgAltPeriod": { + "message": "Illustration of a list of logins that are at-risk." }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" }, - "generatePasswordSlideImgAlt": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password" + "generatePasswordSlideImgAltPeriod": { + "message": "Illustration of the Bitwarden autofill menu displaying a generated password." }, "updateInBitwarden": { "message": "Update in Bitwarden" @@ -2551,8 +2521,8 @@ "message": "Bitwarden will then prompt you to update the password in the password manager.", "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, - "updateInBitwardenSlideImgAlt": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login" + "updateInBitwardenSlideImgAltPeriod": { + "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." }, "turnOnAutofill": { "message": "Turn on autofill" @@ -3007,6 +2977,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -3839,15 +3818,9 @@ "duoHealthCheckResultsInNullAuthUrlError": { "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." }, - "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." - }, "duoRequiredForAccount": { "message": "Duo two-step login is required for your account." }, - "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." - }, "popoutExtension": { "message": "Popout extension" }, @@ -4270,6 +4243,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4267,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1381afbdc6e..9c6d212f8c9 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organisation vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organisation vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Auto-fill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 6b14a358148..aa7a234246f 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organisation vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organisation vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Auto-fill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 4d430d23337..d282011e628 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exportando caja fuerte de la organización" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autocompletar - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 2daf5f9d7f0..e2ad9a1e53a 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index b1e1ca3526b..95d843a3aa8 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 2b3d66ad104..0f5616cd001 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index a37bd0235c1..ff71b93a62d 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Organisaation holvin vienti" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Automaattitäytä - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopioi $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 9f991843f4f..292c5fd0576 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 32b6dd0296b..5ec06f52a76 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Export du coffre de l'organisation" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Saisie automatique - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copier $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 6c0b9cce87b..c76c60114c9 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exportar Caixa forte da organización" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autoenchido - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 035bf9da48e..0d15c90c3d1 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "מייצא כספת ארגון" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "מילוי אוטומטי - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "העתק $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 748d9eb966b..6dc3dced829 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "केवल $EMAIL$ से जुड़े अनुलग्नकों सहित व्यक्तिगत वॉल्ट आइटम ही निर्यात किए जाएंगे. संगठन वॉल्ट आइटम शामिल नहीं किए जाएंगे", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index b88fc45493f..eda6d7267a3 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Izvest će se samo stavke i privici osobnog trezora povezanog s $EMAIL$. Stavke organizacijskog trezora neće biti uključene", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Izvoz organizacijskog trezora" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Auto-ispuna - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopiraj $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 80b1dbf095d..ec313c2cd10 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Csak $EMAIL$ email címmel társított személyes széf elemek kerülnek exportálásra. Ebbe nem kerülnek be a szervezeti széf elemek.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Szervezeti széf exportálása" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Automatikus kitöltés - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "$FIELD$, $VALUE$ másolása", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 0146fb2a000..daccc3d8272 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -81,7 +81,7 @@ "message": "Petunjuk Kata Sandi Utama (opsional)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Skor kekuatan kata sandi $SCORE$", "placeholders": { "score": { "content": "$1", @@ -656,10 +656,10 @@ "message": "Verifikasikan identitas Anda" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "Kami tidak mengenali perangkat ini. Masukkan kode yang dikirim ke surel Anda untuk memverifikasi identitas Anda." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "Lanjutkan log masuk" }, "yourVaultIsLocked": { "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." @@ -869,19 +869,19 @@ "message": "Masuk ke Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Masukkan kode yang dikirim ke surel Anda" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Masukkan kode dari aplikasi autentikator Anda" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "Sentuh YubiKey Anda untuk mengautentikasi" }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "Log masuk dua langkah berganda diperlukan bagi akun Anda. Ikuti langkah di bawah untuk menyelesaikan log masuk." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "Ikuti langkah-langkah di bawah untuk menyelesaikan log masuk." }, "restartRegistration": { "message": "Mulai ulang pendaftaran" @@ -1019,7 +1019,7 @@ "message": "Tanyakan untuk menambah sebuah benda jika benda itu tidak ditemukan di brankas Anda. Diterapkan ke seluruh akun yang telah masuk." }, "showCardsInVaultViewV2": { - "message": "Always show cards as Autofill suggestions on Vault view" + "message": "Selalu tampilan kartu sebagai saran isi otomatis pada tampilan Brankas" }, "showCardsCurrentTab": { "message": "Tamplikan kartu pada halaman Tab" @@ -1028,7 +1028,7 @@ "message": "Buat tampilan daftar benda dari kartu pada halaman Tab untuk isi otomatis yang mudah." }, "showIdentitiesInVaultViewV2": { - "message": "Always show identities as Autofill suggestions on Vault view" + "message": "Selalu tampilan identitas sebagai saran isi otomatis pada tampilan Brankas" }, "showIdentitiesCurrentTab": { "message": "Tampilkan identitas pada halaman Tab" @@ -1037,10 +1037,10 @@ "message": "Buat tampilan daftar benda dari identitas pada halaman Tab untuk isi otomatis yang mudah." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Klik butir untuk mengisi otomatis pada tampilan Brankas" }, "clickToAutofill": { - "message": "Click items in autofill suggestion to fill" + "message": "Klik butir dalam saran isi otomatis untuk mengisi" }, "clearClipboard": { "message": "Hapus Papan Klip", @@ -1057,7 +1057,7 @@ "message": "Iya, Simpan Sekarang" }, "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", + "message": "$USERNAME$ disimpan ke Bitwarden.", "placeholders": { "username": { "content": "$1" @@ -1066,7 +1066,7 @@ "description": "Shown to user after login is saved." }, "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", + "message": "$USERNAME$ diperbarui di Bitwarden.", "placeholders": { "username": { "content": "$1" @@ -1075,35 +1075,35 @@ "description": "Shown to user after login is updated." }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "Simpan sebagai log masuk baru", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "Perbarui log masuk", "description": "Button text for updating an existing login entry." }, "saveLoginPrompt": { - "message": "Save login?", + "message": "Simpan log masuk?", "description": "Prompt asking the user if they want to save their login details." }, "updateLoginPrompt": { - "message": "Update existing login?", + "message": "Perbarui log masuk yang ada?", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "Log masuk disimpan", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "Log masuk diperbarui", "description": "Message displayed when login details are successfully updated." }, "saveFailure": { - "message": "Error saving", + "message": "Kesalahan saat menyimpan", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "Oh tidak! Kami tidak bisa menyimpan ini. Cobalah memasukkan rincian secara manual.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -1422,7 +1422,7 @@ "message": "Ingat saya" }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "Jangan tanyakan lagi pada perangkat ini untuk 30 hari" }, "sendVerificationCodeEmailAgain": { "message": "Kirim ulang email kode verifikasi" @@ -1431,11 +1431,11 @@ "message": "Gunakan metode masuk dua langkah lainnya" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "Pilih metode lain", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Gunakan kode pemulihan Anda" }, "insertYubiKey": { "message": "Masukkan YubiKey Anda ke port USB komputer Anda, lalu sentuh tombolnya." @@ -1450,16 +1450,16 @@ "message": "Buka tab baru" }, "openInNewTab": { - "message": "Open in new tab" + "message": "Buka dalam tab baru" }, "webAuthnAuthenticate": { "message": "Autentikasi dengan WebAuthn." }, "readSecurityKey": { - "message": "Read security key" + "message": "Baca kunci keamanan" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "Menunggu interaksi kunci keamanan..." }, "loginUnavailable": { "message": "Info Masuk Tidak Tersedia" @@ -1474,7 +1474,7 @@ "message": "Opsi Info Masuk Dua Langkah" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "Pilih metode log masuk dua langkah" }, "recoveryCodeDesc": { "message": "Kehilangan akses ke semua penyedia dua faktor Anda? Gunakan kode pemulihan untuk menonaktifkan semua penyedia dua faktor dari akun Anda." @@ -1668,7 +1668,7 @@ "message": "Seret untuk mengurutkan" }, "dragToReorder": { - "message": "Drag to reorder" + "message": "Seret untuk mengubah urutan" }, "cfTypeText": { "message": "Teks" @@ -2144,7 +2144,7 @@ "message": "Pembuat nama pengguna" }, "useThisEmail": { - "message": "Use this email" + "message": "Pakai surel ini" }, "useThisPassword": { "message": "Gunakan kata sandi ini" @@ -2164,7 +2164,7 @@ "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultCustomization": { - "message": "Vault customization" + "message": "Penyesuaian brankas" }, "vaultTimeoutAction": { "message": "Tindakan Batas Waktu Brankas" @@ -2173,13 +2173,13 @@ "message": "Batas waktu tindakan" }, "newCustomizationOptionsCalloutTitle": { - "message": "New customization options" + "message": "Opsi penyesuaian baru" }, "newCustomizationOptionsCalloutContent": { - "message": "Customize your vault experience with quick copy actions, compact mode, and more!" + "message": "Sesuaikan pengalaman brankas Anda dengan aksi salin cepat, mode kompak, dan lainnya!" }, "newCustomizationOptionsCalloutLink": { - "message": "View all Appearance settings" + "message": "Lihat semua pengaturan Penampilan" }, "lock": { "message": "Kunci", @@ -2437,10 +2437,10 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Domain terblokir" }, "learnMoreAboutBlockedDomains": { - "message": "Learn more about blocked domains" + "message": "Pelajari lebih lanjut tentang domain yang diblokir" }, "excludedDomains": { "message": "Domain yang Dikecualikan" @@ -2452,19 +2452,19 @@ "message": "Bitwarden tidak akan meminta untuk menyimpan rincian login untuk domain tersebut. Anda harus menyegarkan halaman agar perubahan diterapkan." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Isi otomatis dan fitur terkait lain tidak akan ditawarkan bagi situs-situs web ini. Anda mesti menyegarkan halaman agar perubahan berdampak." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Isi otomatis diblokir bagi situs web ini." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Ubah ini di pengaturan" }, "change": { "message": "Ubah" }, "changeButtonTitle": { - "message": "Change password - $ITEMNAME$", + "message": "Ubah kata sandi - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -2473,10 +2473,10 @@ } }, "atRiskPasswords": { - "message": "At-risk passwords" + "message": "Kata sandi yang berrisiko" }, "atRiskPasswordDescSingleOrg": { - "message": "$ORGANIZATION$ is requesting you change one password because it is at-risk.", + "message": "$ORGANIZATION$ meminta Ada mengubah satu kata sandi karena itu berrisiko.", "placeholders": { "organization": { "content": "$1", @@ -2485,7 +2485,7 @@ } }, "atRiskPasswordsDescSingleOrgPlural": { - "message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "$ORGANIZATION$ meminta Anda mengubah $COUNT$ kata sandi karena mereka berrisiko.", "placeholders": { "organization": { "content": "$1", @@ -2498,7 +2498,7 @@ } }, "atRiskPasswordsDescMultiOrgPlural": { - "message": "Your organizations are requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "Organisasi Anda meminta Anda mengubah $COUNT$ kata sandi karena mereka berrisiko.", "placeholders": { "count": { "content": "$1", @@ -2507,10 +2507,10 @@ } }, "reviewAndChangeAtRiskPassword": { - "message": "Review and change one at-risk password" + "message": "Tinjau dan ubah satu kata sandi berrisiko" }, "reviewAndChangeAtRiskPasswordsPlural": { - "message": "Review and change $COUNT$ at-risk passwords", + "message": "Tinjau dan ubah $COUNT$ kata sandi berrisiko", "placeholders": { "count": { "content": "$1", @@ -2519,7 +2519,7 @@ } }, "changeAtRiskPasswordsFaster": { - "message": "Change at-risk passwords faster" + "message": "Ubah lebih cepat kata sandi yang berrisiko" }, "changeAtRiskPasswordsFasterDesc": { "message": "Update your settings so you can quickly autofill your passwords and generate new ones" @@ -2528,14 +2528,14 @@ "message": "Review at-risk logins" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords" + "message": "Tinjau kata sandi yang berrisiko" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.", + "message": "Kata sandi organisasi Anda berrisiko karena mereka lemah, dipakai ulang, dan/atau terpapar.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAlt": { - "message": "Illustration of a list of logins that are at-risk" + "message": "Ilustrasi dari daftar log masuk yang berrisiko" }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", @@ -2561,7 +2561,7 @@ "message": "Turned on autofill" }, "dismiss": { - "message": "Dismiss" + "message": "Tutup" }, "websiteItemLabel": { "message": "Situs web $number$ (URI)", @@ -2582,7 +2582,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Perubahan domain yang diblokir disimpan" }, "excludedDomainsSavedSuccess": { "message": "Perubahan domain yang diabaikan telah disimpan" @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Mengekspor brankas organisasi" }, @@ -3023,17 +3032,17 @@ "message": "Galat" }, "decryptionError": { - "message": "Decryption error" + "message": "Kesalahan dekripsi" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden tidak bisa mendekripsi butir brankas yang tercantum di bawah." }, "contactCSToAvoidDataLossPart1": { "message": "Contact customer success", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "untuk menghindari lanjutan hilang data.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3168,7 +3177,7 @@ } }, "forwaderInvalidOperation": { - "message": "$SERVICENAME$ refused your request. Please contact your service provider for assistance.", + "message": "$SERVICENAME$ menolak permintaan Anda. Harap hubungi penyedia layanan Anda untuk bantuan.", "description": "Displayed when the user is forbidden from using the API by the forwarding service.", "placeholders": { "servicename": { @@ -3178,7 +3187,7 @@ } }, "forwaderInvalidOperationWithMessage": { - "message": "$SERVICENAME$ refused your request: $ERRORMESSAGE$", + "message": "$SERVICENAME$ menolak permintaan Anda: $ERRORMESSAGE$", "description": "Displayed when the user is forbidden from using the API by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3327,13 +3336,13 @@ "message": "Sebuah pemberitahuan dikirim ke perangkat Anda." }, "notificationSentDevicePart1": { - "message": "Unlock Bitwarden on your device or on the" + "message": "Buka kunci Bitwarden pada perangkat Anda atau pada" }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "aplikasi web" }, "notificationSentDevicePart2": { - "message": "Make sure the Fingerprint phrase matches the one below before approving." + "message": "Pastikan frasa Sidik Jari cocok dengan yang di bawah sebelum menyetujui." }, "aNotificationWasSentToYourDevice": { "message": "Sebuah pemberitahuan telah dikirim ke perangkat Anda" @@ -3348,7 +3357,7 @@ "message": "Memulai login" }, "logInRequestSent": { - "message": "Request sent" + "message": "Permintaan terkirim" }, "exposedMasterPassword": { "message": "Kata Sandi Utama yang Terpapar" @@ -4080,7 +4089,7 @@ "message": "Akun aktif" }, "bitwardenAccount": { - "message": "Bitwarden account" + "message": "Akun Bitwarden" }, "availableAccounts": { "message": "Akun yang tersedia" @@ -4203,10 +4212,10 @@ "message": "Kunci sandi dihapus" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Saran isi otomatis" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Butir yang disarankan" }, "autofillSuggestionsTip": { "message": "Simpan benda login untuk situs ini ke isi otomatis" @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Isi otomatis - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,8 +4303,22 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { - "message": "Copy $FIELD$, $VALUE$", + "message": "Salin $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4769,7 +4806,7 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "Lokasi Item" }, "fileSend": { "message": "File Send" @@ -4835,7 +4872,7 @@ "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { - "message": "Show character count" + "message": "Tunjukkan cacah karakter" }, "hideCharacterCount": { "message": "Hide character count" @@ -4896,15 +4933,15 @@ "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Kata sandi dibuat ulang", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Simpan log masuk ke Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Spasi", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { @@ -5051,22 +5088,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Pemberitahuan penting" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Siapkan log masuk dua langkah" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden akan mengirim suatu kode ke akun surel Anda untuk memverifikasi log masuk dari perangkat baru sejak Februari 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Anda dapat menyiapkan log masuk dua langkah sebagai cara alternatif untuk melindungi akun Anda atau mengubah surel Anda ke yang bisa Anda akses." }, "remindMeLater": { - "message": "Remind me later" + "message": "Ingatkan saya nanti" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Apakah Anda punya akses yang handal ke surel Anda, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -5081,7 +5118,7 @@ "message": "Ya, saya dapat mengakses surel saya secara handla" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Nyalakan log masuk dua langkah" }, "changeAcctEmail": { "message": "Ubah surel akun" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index cb8e414ca37..b18acbc79b8 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Esportando cassaforte dell'organizzazione" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Riempi automaticamente - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copia $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index b67d0b62a6e..a6dd0f709a2 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "$EMAIL$ に関連付けられた個人用保管庫のアイテムのみが、添付ファイルを含めてエクスポートされます。組織用保管庫のアイテムは含まれません。", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "組織保管庫のエクスポート" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "自動入力 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "$FIELD$ 「$VALUE$」 をコピー", "description": "Title for a button that copies a field value to the clipboard.", @@ -5066,7 +5103,7 @@ "message": "後で再通知" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "新しいメールアドレス $EMAIL$ はあなたが管理しているものですか?", + "message": "メールアドレス $EMAIL$ は、確実にアクセスできるものですか?", "placeholders": { "email": { "content": "$1", diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 818d7cdcd19..0e11594faac 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index c9c29611deb..0c087ef7de9 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index c8aff3a6488..31ea7daa668 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index cd54ac47506..92654f84e31 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "조직 보관함을 내보내는 중" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "자동 완성 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 8bf8a8f7518..e1536abdf83 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 7b5f077f69d..789666ef2c1 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Tiks izdoti tikai atsevišķi glabātavas vienumi, tajā skaitā pielikumi, kas ir saistīti ar $EMAIL$. Apvienības glabātavas vienumi netiks iekļauti", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Izgūst apvienības glabātavu" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Apskatīt vienumu - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Automātiski aizpildīt - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Automātiskā aizpilde - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Ievietot starpliktuvē $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 24a096db0ef..11af6b54202 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 9a49998d3d9..472a8378bb7 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index c9c29611deb..0c087ef7de9 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 0ff4ed9486a..9a52eeea4cb 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autoutfyll - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index c9c29611deb..0c087ef7de9 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 1c2b09ca3e3..80877839adb 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Exporteert alleen de persoonlijke kluis-items, inclusief attachments, gerelateerd aan $EMAIL$. Geen kluis-items van de organisatie", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Organisatiekluis exporteren" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Item weergeven - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Automatisch invullen - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Automatisch aanvullen - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "$FIELD$, $VALUE$ kopiëren", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index c9c29611deb..0c087ef7de9 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index c9c29611deb..0c087ef7de9 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index f4c1303bd18..c345972bd4a 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Tylko poszczególne elementy sejfu łącznie z załącznikami powiązanymi z $EMAIL$ zostaną wyeksportowane. Elementy sejfu organizacji nie będą dołączone", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Eksportowanie sejfu organizacji" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Zobacz element - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autouzupełnij - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autouzupełnij - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopiuj $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0b5d569f443..5aacad23a93 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exportando cofre da organização" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Auto-preenchimento - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copiar $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index c1f1f51609c..0e467b65183 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Apenas os itens individuais do cofre, incluindo os anexos associados a $EMAIL$, serão exportados. Os itens do cofre da organização não serão incluídos", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "A exportar o cofre da organização" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Ver item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Preencher automaticamente - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Preencher automaticamente - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copiar $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 5ce73eb2e49..fa4d754e99d 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 11d8de4acbe..c775bee3289 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Будут экспортированы только отдельные элементы хранилища, включая вложения, связанные с $EMAIL$. Элементы хранилища организации включены не будут", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Экспорт хранилища организации" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Автозаполнение - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Скопировать $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 721d16a2eee..12bac21e11f 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 897db9f3176..cd4095f8ff5 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Exportované budú iba položky osobného trezora spojené s $EMAIL$. Položky trezora organizácie nebudú zahrnuté v exporte", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exportovanie trezora organizácie" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Zobraziť položku - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Automatické vyplnenie - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Automatické vyplnenie - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopírovať $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index f6a543ea5ea..c77d4cb55f4 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 3c7bef94c13..5e325c6a97c 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Извоз сефа организације" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Ауто-пуњење - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Копирај $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 6db7a22490e..e3ff6986cae 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index c9c29611deb..0c087ef7de9 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index c545f802d64..e2225327664 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Autofill - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index e69b33d63af..e8788a00d70 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Yalnızca $EMAIL$ ile ilişkili kişisel kasadaki kayıtlar ve dosyalar dışa aktarılacaktır. Kuruluş kasasındaki kayıtlar dahil edilmeyecektir", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Kuruluş kasasını dışa aktarma" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "Kaydı görüntüle - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Otomatik doldur - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Otomatik doldur - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Kopyala: $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index d18d90babff..6bd9ca7fc36 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Експортування сховища організації" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Автозаповнення – $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Копіювати $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index db0da3b5874..35c553f72f1 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "Đang xuất dữ liệu kho tổ chức" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "Tự động điền - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index bdfaeb92531..fc6f70cb6a4 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "仅会导出与 $EMAIL$ 关联的个人密码库项目(包括附件)。不包括组织密码库项目。", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "正在导出组织密码库" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "查看项目 - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "自动填充 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "自动填充 - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "复制 $FIELD$,$VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 37e450c8fd2..365aae61ebf 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3007,6 +3007,15 @@ } } }, + "exportingIndividualVaultWithAttachmentsDescription": { + "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "exportingOrganizationVaultTitle": { "message": "正在匯出組織密碼庫" }, @@ -4270,6 +4279,20 @@ } } }, + "viewItemTitleWithField": { + "message": "View item - $ITEMNAME$ - $FIELD$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "autofillTitle": { "message": "自動填入 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", @@ -4280,6 +4303,20 @@ } } }, + "autofillTitleWithField": { + "message": "Autofill - $ITEMNAME$ - $FIELD$", + "description": "Title for a button that autofills a login item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "field": { + "content": "$2", + "example": "Username" + } + } + }, "copyFieldValue": { "message": "Copy $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", diff --git a/apps/browser/src/auth/popup/components/set-pin.component.html b/apps/browser/src/auth/popup/components/set-pin.component.html index 80e1b63c7d7..58cb42456ee 100644 --- a/apps/browser/src/auth/popup/components/set-pin.component.html +++ b/apps/browser/src/auth/popup/components/set-pin.component.html @@ -25,13 +25,13 @@ {{ "lockWithMasterPassOnRestart1" | i18n }} -
+ -
+ diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 88a3b1c3076..54cb5203a87 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -21,6 +21,7 @@ [hideLogo]="true" [maxWidth]="maxWidth" [hideFooter]="hideFooter" + [hideIcon]="hideIcon" > diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 10ef65d0654..51dbb6503d7 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -26,6 +26,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { showBackButton?: boolean; showLogo?: boolean; hideFooter?: boolean; + hideIcon?: boolean; } @Component({ @@ -48,6 +49,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected showAcctSwitcher: boolean; protected showBackButton: boolean; protected showLogo: boolean = true; + protected hideIcon: boolean = false; protected pageTitle: string; protected pageSubtitle: string; @@ -129,6 +131,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { if (firstChildRouteData["showLogo"] !== undefined) { this.showLogo = Boolean(firstChildRouteData["showLogo"]); } + + if (firstChildRouteData["hideIcon"] !== undefined) { + this.hideIcon = Boolean(firstChildRouteData["hideIcon"]); + } } private listenForServiceDataChanges() { @@ -180,6 +186,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { if (data.showLogo !== undefined) { this.showLogo = data.showLogo; } + + if (data.hideIcon !== undefined) { + this.hideIcon = data.hideIcon; + } } private handleStringOrTranslation(value: string | Translation): string { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index 841eefda0ad..a0990485d49 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -242,6 +242,7 @@ const initialData: ExtensionAnonLayoutWrapperData = { showAcctSwitcher: true, showBackButton: true, showLogo: true, + hideIcon: false, }; const changedData: ExtensionAnonLayoutWrapperData = { @@ -255,6 +256,7 @@ const changedData: ExtensionAnonLayoutWrapperData = { showAcctSwitcher: false, showBackButton: false, showLogo: false, + hideIcon: false, }; @Component({ diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index 3c5fd7a6af8..b8252aa6e13 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -95,7 +95,7 @@ - +

{{ "otherOptions" | i18n }}

diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index 60b38570482..abe642970bb 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -99,7 +99,7 @@ describe("AccountSecurityComponent", () => { it("pin enabled when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); @@ -111,7 +111,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -129,7 +129,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -143,7 +143,7 @@ describe("AccountSecurityComponent", () => { it("pin visible when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); fixture.detectChanges(); @@ -158,7 +158,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); @@ -173,7 +173,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); pinServiceAbstraction.isPinSet.mockResolvedValue(true); @@ -190,7 +190,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 8cdfdab9524..66e5b0bb214 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -27,8 +27,10 @@ import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitward import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeout, @@ -152,8 +154,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); this.showMasterPasswordOnClientRestartOption = hasMasterPassword; - const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout); - if ((await firstValueFrom(this.policyService.get$(PolicyType.MaximumVaultTimeout))) != null) { + const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), + ), + getFirstPolicy, + ); + if ((await firstValueFrom(maximumVaultTimeoutPolicy)) != null) { this.hasVaultTimeoutPolicy = true; } @@ -195,7 +203,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { timeout = VaultTimeoutStringType.OnRestart; } - this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe( + this.pinEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId), + ), + getFirstPolicy, map((policy) => { return policy == null || !policy.enabled; }), @@ -220,11 +233,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { .pipe( switchMap(async () => { const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id); - const biometricSettingAvailable = - !(await BrowserApi.permissionsGranted(["nativeMessaging"])) || - (status !== BiometricsStatus.DesktopDisconnected && - status !== BiometricsStatus.NotEnabledInConnectedDesktopApp) || - (await this.vaultTimeoutSettingsService.isBiometricLockSet()); + const biometricSettingAvailable = await this.biometricsService.canEnableBiometricUnlock(); if (!biometricSettingAvailable) { this.form.controls.biometric.disable({ emitEvent: false }); } else { @@ -243,6 +252,13 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { "biometricsStatusHelptextNotEnabledInDesktop", activeAccount.email, ); + } else if ( + status === BiometricsStatus.HardwareUnavailable && + !biometricSettingAvailable + ) { + this.biometricUnavailabilityReason = this.i18nService.t( + "biometricsStatusHelptextHardwareUnavailable", + ); } else { this.biometricUnavailabilityReason = ""; } diff --git a/apps/browser/src/auth/popup/two-factor-options-v1.component.html b/apps/browser/src/auth/popup/two-factor-options-v1.component.html deleted file mode 100644 index f25944aba65..00000000000 --- a/apps/browser/src/auth/popup/two-factor-options-v1.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- -
-

- {{ "twoStepOptions" | i18n }} -

-
-
-
-
-
- - -
-
-
diff --git a/apps/browser/src/auth/popup/two-factor-options-v1.component.ts b/apps/browser/src/auth/popup/two-factor-options-v1.component.ts deleted file mode 100644 index 0c71421fc04..00000000000 --- a/apps/browser/src/auth/popup/two-factor-options-v1.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; - -import { TwoFactorOptionsComponentV1 as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options-v1.component"; -import { - TwoFactorProviderDetails, - TwoFactorService, -} from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -@Component({ - selector: "app-two-factor-options", - templateUrl: "two-factor-options-v1.component.html", -}) -export class TwoFactorOptionsComponentV1 extends BaseTwoFactorOptionsComponent { - constructor( - twoFactorService: TwoFactorService, - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - private activatedRoute: ActivatedRoute, - ) { - super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService); - } - - close() { - this.navigateTo2FA(); - } - - override async choose(p: TwoFactorProviderDetails) { - await super.choose(p); - await this.twoFactorService.setSelectedProvider(p.type); - - this.navigateTo2FA(); - } - - navigateTo2FA() { - const sso = this.activatedRoute.snapshot.queryParamMap.get("sso") === "true"; - - if (sso) { - // Persist SSO flag back to the 2FA comp if it exists - // in order for successful login logic to work properly for - // SSO + 2FA in browser extension - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["2fa"], { queryParams: { sso: true } }); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["2fa"]); - } - } -} diff --git a/apps/browser/src/auth/popup/two-factor-v1.component.html b/apps/browser/src/auth/popup/two-factor-v1.component.html deleted file mode 100644 index 126b0ea5a99..00000000000 --- a/apps/browser/src/auth/popup/two-factor-v1.component.html +++ /dev/null @@ -1,196 +0,0 @@ -
-
-
- -
-

- {{ title }} -

-
- -
-
-
- - -
- - {{ "enterVerificationCodeApp" | i18n }} - - - {{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }} - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-

{{ "insertYubiKey" | i18n }}

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

{{ "webAuthnNewTab" | i18n }}

- -
-
- - -
-

- {{ "duoRequiredForAccount" | i18n }} -

- -

- {{ "popoutTheExtensionToCompleteLogin" | i18n }} -

- - -

{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}

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

{{ "noTwoStepProviders" | i18n }}

-

{{ "noTwoStepProviders2" | i18n }}

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

- -

-
-
-
diff --git a/apps/browser/src/auth/popup/two-factor-v1.component.ts b/apps/browser/src/auth/popup/two-factor-v1.component.ts deleted file mode 100644 index 884e42bf73a..00000000000 --- a/apps/browser/src/auth/popup/two-factor-v1.component.ts +++ /dev/null @@ -1,260 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, Subscription, firstValueFrom } from "rxjs"; -import { filter, first, takeUntil } from "rxjs/operators"; - -import { TwoFactorComponentV1 as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor-v1.component"; -import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { BrowserApi } from "../../platform/browser/browser-api"; -import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; - -import { closeTwoFactorAuthWebAuthnPopout } from "./utils/auth-popout-window"; - -@Component({ - selector: "app-two-factor", - templateUrl: "two-factor-v1.component.html", -}) -export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); - inPopout = BrowserPopupUtils.inPopout(window); - - constructor( - loginStrategyService: LoginStrategyServiceAbstraction, - router: Router, - i18nService: I18nService, - apiService: ApiService, - platformUtilsService: PlatformUtilsService, - private syncService: SyncService, - environmentService: EnvironmentService, - stateService: StateService, - route: ActivatedRoute, - private messagingService: MessagingService, - logService: LogService, - twoFactorService: TwoFactorService, - appIdService: AppIdService, - loginEmailService: LoginEmailServiceAbstraction, - userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigService, - ssoLoginService: SsoLoginServiceAbstraction, - private dialogService: DialogService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - toastService: ToastService, - @Inject(WINDOW) protected win: Window, - private browserMessagingApi: ZonedMessageListenerService, - ) { - super( - loginStrategyService, - router, - i18nService, - apiService, - platformUtilsService, - win, - environmentService, - stateService, - route, - logService, - twoFactorService, - appIdService, - loginEmailService, - userDecryptionOptionsService, - ssoLoginService, - configService, - masterPasswordService, - accountService, - toastService, - ); - this.onSuccessfulLogin = async () => { - // 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 - syncService.fullSync(true); - }; - - this.onSuccessfulLoginTde = async () => { - // 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 - syncService.fullSync(true); - }; - - this.onSuccessfulLoginTdeNavigate = async () => { - this.win.close(); - }; - - this.successRoute = "/tabs/vault"; - // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe - this.webAuthnNewTab = true; - } - - async ngOnInit() { - if (this.route.snapshot.paramMap.has("webAuthnResponse")) { - // WebAuthn fallback response - this.selectedProviderType = TwoFactorProviderType.WebAuthn; - this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); - this.onSuccessfulLogin = async () => { - // 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.syncService.fullSync(true); - this.messagingService.send("reloadPopup"); - window.close(); - }; - this.remember = this.route.snapshot.paramMap.get("remember") === "true"; - await this.doSubmit(); - return; - } - - await super.ngOnInit(); - if (this.selectedProviderType == null) { - return; - } - - // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width - // than usual to avoid cutting off the dialog. - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { - document.body.classList.add("linux-webauthn"); - } - - if ( - this.selectedProviderType === TwoFactorProviderType.Email && - BrowserPopupUtils.inPopup(window) - ) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "warning" }, - content: { key: "popup2faCloseMessage" }, - type: "warning", - }); - if (confirmed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.openCurrentPagePopout(window); - } - } - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - if (qParams.sso === "true") { - this.onSuccessfulLogin = async () => { - // This is not awaited so we don't pause the application while the sync is happening. - // This call is executed by the service that lives in the background script so it will continue - // the sync even if this tab closes. - // 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.syncService.fullSync(true); - - // Force sidebars (FF && Opera) to reload while exempting current window - // because we are just going to close the current window. - BrowserApi.reloadOpenWindows(true); - - // We don't need this window anymore because the intent is for the user to be left - // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) - await closeTwoFactorAuthWebAuthnPopout(); - }; - } - }); - } - - async ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { - document.body.classList.remove("linux-webauthn"); - } - super.ngOnDestroy(); - } - - anotherMethod() { - const sso = this.route.snapshot.queryParamMap.get("sso") === "true"; - - if (sso) { - // We must persist this so when the user returns to the 2FA comp, the - // proper onSuccessfulLogin logic is executed. - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["2fa-options"], { queryParams: { sso: true } }); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["2fa-options"]); - } - } - - async popoutCurrentPage() { - await BrowserPopupUtils.openCurrentPagePopout(window); - } - - async isLinux() { - return (await BrowserApi.getPlatformInfo()).os === "linux"; - } - - duoResultSubscription: Subscription; - protected override setupDuoResultListener() { - if (!this.duoResultSubscription) { - this.duoResultSubscription = this.browserMessagingApi - .messageListener$() - .pipe( - filter((msg: any) => msg.command === "duoResult"), - takeUntil(this.destroy$), - ) - .subscribe((msg: { command: string; code: string; state: string }) => { - this.token = msg.code + "|" + msg.state; - // This floating promise is intentional. We don't need to await the submit + awaiting in a subscription is not recommended. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.submit(); - }); - } - } - - override async launchDuoFrameless() { - if (this.duoFramelessUrl === null) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), - }); - return; - } - - const duoHandOffMessage = { - title: this.i18nService.t("youSuccessfullyLoggedIn"), - message: this.i18nService.t("youMayCloseThisWindow"), - isCountdown: false, - }; - - // we're using the connector here as a way to set a cookie with translations - // before continuing to the duo frameless url - const env = await firstValueFrom(this.environmentService.environment$); - const launchUrl = - env.getWebVaultUrl() + - "/duo-redirect-connector.html" + - "?duoFramelessUrl=" + - encodeURIComponent(this.duoFramelessUrl) + - "&handOffMessage=" + - encodeURIComponent(JSON.stringify(duoHandOffMessage)); - this.platformUtilsService.launchUri(launchUrl); - } -} diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts index a300ac08660..9f197b02193 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -8,6 +8,9 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; @@ -35,10 +38,12 @@ describe("AutoSubmitLoginBackground", () => { let configService: MockProxy; let platformUtilsService: MockProxy; let policyDetails: MockProxy; - let automaticAppLogInPolicy$: BehaviorSubject; - let policyAppliesToActiveUser$: BehaviorSubject; + let automaticAppLogInPolicy$: BehaviorSubject; + let policyAppliesToUser$: BehaviorSubject; let policyService: MockProxy; let autoSubmitLoginBackground: AutoSubmitLoginBackground; + let accountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; const validIpdUrl1 = "https://example.com"; const validIpdUrl2 = "https://subdomain.example3.com"; const validAutoSubmitHost = "some-valid-url.com"; @@ -61,12 +66,13 @@ describe("AutoSubmitLoginBackground", () => { idpHost: `${validIpdUrl1} , https://example2.com/some/sub-route ,${validIpdUrl2}, [invalidValue] ,,`, }, }); - automaticAppLogInPolicy$ = new BehaviorSubject(policyDetails); - policyAppliesToActiveUser$ = new BehaviorSubject(true); + automaticAppLogInPolicy$ = new BehaviorSubject([policyDetails]); + policyAppliesToUser$ = new BehaviorSubject(true); policyService = mock({ - get$: jest.fn().mockReturnValue(automaticAppLogInPolicy$), - policyAppliesToActiveUser$: jest.fn().mockReturnValue(policyAppliesToActiveUser$), + policiesByType$: jest.fn().mockReturnValue(automaticAppLogInPolicy$), + policyAppliesToUser$: jest.fn().mockReturnValue(policyAppliesToUser$), }); + accountService = mockAccountServiceWith(mockUserId); autoSubmitLoginBackground = new AutoSubmitLoginBackground( logService, autofillService, @@ -75,6 +81,7 @@ describe("AutoSubmitLoginBackground", () => { configService, platformUtilsService, policyService, + accountService, ); }); @@ -84,7 +91,7 @@ describe("AutoSubmitLoginBackground", () => { describe("when the AutoSubmitLoginBackground feature is disabled", () => { it("destroys all event listeners when the AutomaticAppLogIn policy is not enabled", async () => { - automaticAppLogInPolicy$.next(mock({ ...policyDetails, enabled: false })); + automaticAppLogInPolicy$.next([mock({ ...policyDetails, enabled: false })]); await autoSubmitLoginBackground.init(); @@ -92,7 +99,7 @@ describe("AutoSubmitLoginBackground", () => { }); it("destroys all event listeners when the AutomaticAppLogIn policy does not apply to the current user", async () => { - policyAppliesToActiveUser$.next(false); + policyAppliesToUser$.next(false); await autoSubmitLoginBackground.init(); @@ -100,7 +107,7 @@ describe("AutoSubmitLoginBackground", () => { }); it("destroys all event listeners when the idpHost is not specified in the AutomaticAppLogIn policy", async () => { - automaticAppLogInPolicy$.next(mock({ ...policyDetails, data: { idpHost: "" } })); + automaticAppLogInPolicy$.next([mock({ ...policyDetails, data: { idpHost: "" } })]); await autoSubmitLoginBackground.init(); @@ -264,6 +271,7 @@ describe("AutoSubmitLoginBackground", () => { configService, platformUtilsService, policyService, + accountService, ); jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(tab); }); diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.ts b/apps/browser/src/autofill/background/auto-submit-login.background.ts index 034938ca521..bce876e8f82 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.ts @@ -1,12 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -42,6 +45,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr private configService: ConfigService, private platformUtilsService: PlatformUtilsService, private policyService: PolicyService, + private accountService: AccountService, ) { this.isSafariBrowser = this.platformUtilsService.isSafari(); } @@ -56,8 +60,14 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr FeatureFlag.IdpAutoSubmitLogin, ); if (featureFlagEnabled) { - this.policyService - .get$(PolicyType.AutomaticAppLogIn) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.AutomaticAppLogIn, userId), + ), + getFirstPolicy, + ) .subscribe(this.handleAutoSubmitLoginPolicySubscription.bind(this)); } } @@ -86,7 +96,12 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr */ private applyPolicyToActiveUser = async (policy: Policy) => { const policyAppliesToUser = await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.AutomaticAppLogIn), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.AutomaticAppLogIn, userId), + ), + ), ); if (!policyAppliesToUser) { diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index d474e303336..ebdd244e140 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -2,7 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; @@ -51,7 +51,7 @@ describe("NotificationBackground", () => { const cipherService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; - const policyService = mock(); + const policyService = mock(); const folderService = mock(); const userNotificationSettingsService = mock(); const domainSettingsService = mock(); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 50e0ee0aa75..c2e90460dfc 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -8,7 +8,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; +import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service"; import { ExtensionCommand, ExtensionCommandType, @@ -743,7 +743,12 @@ export default class NotificationBackground { private async removeIndividualVault(): Promise { return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ), ); } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 340197f6bf3..90a536ad25d 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -154,123 +154,117 @@
- -

{{ "enableAutoFillOnPageLoadSectionTitle" | i18n }}

-
- - - {{ "enableAutoFillOnPageLoadDesc" | i18n }} - {{ "warningCapitalized" | i18n }}: {{ "experimentalFeature" | i18n }} - - {{ "learnMoreAboutAutofillOnPageLoadLinkText" | i18n }} - - - - - {{ "enableAutoFillOnPageLoad" | i18n }} - {{ - "enterprisePolicyRequirementsApplied" | i18n - }} - - - {{ "defaultAutoFillOnPageLoad" | i18n }} - - - {{ "defaultAutoFillOnPageLoadDesc" | i18n }} +
+ + +

{{ "enableAutoFillOnPageLoadSectionTitle" | i18n }}

+
+
+ + + {{ "enableAutoFillOnPageLoadDesc" | i18n }} + {{ "warningCapitalized" | i18n }}: {{ "experimentalFeature" | i18n }} + + {{ "learnMoreAboutAutofillOnPageLoadLinkText" | i18n }} + - - + + + {{ "enableAutoFillOnPageLoad" | i18n }} + {{ + "enterprisePolicyRequirementsApplied" | i18n + }} + + + {{ "defaultAutoFillOnPageLoad" | i18n }} + + + + + + {{ "defaultAutoFillOnPageLoadDesc" | i18n }} + + + +
- - -

{{ "additionalOptions" | i18n }}

-
- - - - {{ "enableContextMenuItem" | i18n }} - - - - {{ "enableAutoTotpCopy" | i18n }} - - - {{ "clearClipboard" | i18n }} - - - {{ "clearClipboardDesc" | i18n }} - - - - {{ "defaultUriMatchDetection" | i18n }} - - - {{ "defaultUriMatchDetectionDesc" | i18n }} - - - + +
+ +

{{ "additionalOptions" | i18n }}

+
+ + + + {{ "enableContextMenuItem" | i18n }} + + + + {{ "enableAutoTotpCopy" | i18n }} + + + {{ "clearClipboard" | i18n }} + + + + + {{ "clearClipboardDesc" | i18n }} + + + + {{ "defaultUriMatchDetection" | i18n }} + + + + + {{ "defaultUriMatchDetectionDesc" | i18n }} + + + +
- + {{ "blockedDomains" | i18n }} diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 7bd6c93bb64..eb18ea9f23e 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -1,8 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { FormsModule } from "@angular/forms"; +import { Component, DestroyRef, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { + FormsModule, + ReactiveFormsModule, + FormBuilder, + FormGroup, + FormControl, +} from "@angular/forms"; import { RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -73,6 +80,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co SectionHeaderComponent, SelectModule, TypographyModule, + ReactiveFormsModule, ], }) export class AutofillComponent implements OnInit { @@ -94,6 +102,18 @@ export class AutofillComponent implements OnInit { protected autofillOnPageLoadFromPolicy$ = this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$; + protected autofillOnPageLoadForm = new FormGroup({ + autofillOnPageLoad: new FormControl(), + defaultAutofill: new FormControl(), + }); + + protected additionalOptionsForm = new FormGroup({ + enableContextMenuItem: new FormControl(), + enableAutoTotpCopy: new FormControl(), + clearClipboard: new FormControl(), + defaultUriMatch: new FormControl(), + }); + enableAutofillOnPageLoad: boolean = false; enableInlineMenu: boolean = false; enableInlineMenuOnIconSelect: boolean = false; @@ -121,10 +141,12 @@ export class AutofillComponent implements OnInit { private messagingService: MessagingService, private vaultSettingsService: VaultSettingsService, private configService: ConfigService, + private formBuilder: FormBuilder, + private destroyRef: DestroyRef, ) { this.autofillOnPageLoadOptions = [ - { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, - { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, + { name: this.i18nService.t("autoFillOnPageLoadYes"), value: true }, + { name: this.i18nService.t("autoFillOnPageLoadNo"), value: false }, ]; this.clearClipboardOptions = [ { name: i18nService.t("never"), value: ClearClipboardDelay.Never }, @@ -181,27 +203,106 @@ export class AutofillComponent implements OnInit { this.inlineMenuVisibility === AutofillOverlayVisibility.OnFieldFocus || this.enableInlineMenuOnIconSelect; + this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + value + ? this.autofillOnPageLoadForm.controls.autofillOnPageLoad.disable({ emitEvent: false }) + : this.autofillOnPageLoadForm.controls.autofillOnPageLoad.enable({ emitEvent: false }); + }); + this.enableAutofillOnPageLoad = await firstValueFrom( this.autofillSettingsService.autofillOnPageLoad$, ); + this.autofillOnPageLoadForm.controls.autofillOnPageLoad.patchValue( + this.enableAutofillOnPageLoad, + { emitEvent: false }, + ); + this.autofillOnPageLoadDefault = await firstValueFrom( this.autofillSettingsService.autofillOnPageLoadDefault$, ); + if (this.enableAutofillOnPageLoad === false) { + this.autofillOnPageLoadForm.controls.defaultAutofill.disable(); + } + + this.autofillOnPageLoadForm.controls.defaultAutofill.patchValue( + this.autofillOnPageLoadDefault, + { emitEvent: false }, + ); + + this.autofillOnPageLoadForm.controls.autofillOnPageLoad.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + void this.autofillSettingsService.setAutofillOnPageLoad(value); + this.enableDefaultAutofillControl(value); + }); + + this.autofillOnPageLoadForm.controls.defaultAutofill.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + void this.autofillSettingsService.setAutofillOnPageLoadDefault(value); + }); + + /** Additional options form */ + this.enableContextMenuItem = await firstValueFrom( this.autofillSettingsService.enableContextMenu$, ); + this.additionalOptionsForm.controls.enableContextMenuItem.patchValue( + this.enableContextMenuItem, + { emitEvent: false }, + ); + this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); + this.additionalOptionsForm.controls.enableAutoTotpCopy.patchValue(this.enableAutoTotpCopy, { + emitEvent: false, + }); + this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); + this.additionalOptionsForm.controls.clearClipboard.patchValue(this.clearClipboard, { + emitEvent: false, + }); + const defaultUriMatch = await firstValueFrom( this.domainSettingsService.defaultUriMatchStrategy$, ); this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; + this.additionalOptionsForm.controls.defaultUriMatch.patchValue(this.defaultUriMatch, { + emitEvent: false, + }); + + this.additionalOptionsForm.controls.enableContextMenuItem.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + void this.autofillSettingsService.setEnableContextMenu(value); + this.messagingService.send("bgUpdateContextMenu"); + }); + + this.additionalOptionsForm.controls.enableAutoTotpCopy.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + void this.autofillSettingsService.setAutoCopyTotp(value); + }); + + this.additionalOptionsForm.controls.clearClipboard.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + void this.autofillSettingsService.setClearClipboardDelay(value); + }); + + this.additionalOptionsForm.controls.defaultUriMatch.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + void this.domainSettingsService.setDefaultUriMatchStrategy(value); + }); + const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); await this.setAutofillKeyboardHelperText(command); @@ -230,17 +331,16 @@ export class AutofillComponent implements OnInit { await this.requestPrivacyPermission(); } } - - async updateAutofillOnPageLoad() { - await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutofillOnPageLoad); + async getAutofillOnPageLoadFromPolicy() { + await firstValueFrom(this.autofillOnPageLoadFromPolicy$); } - async updateAutofillOnPageLoadDefault() { - await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autofillOnPageLoadDefault); - } - - async saveDefaultUriMatch() { - await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch); + enableDefaultAutofillControl(enable: boolean = true) { + if (enable) { + this.autofillOnPageLoadForm.controls.defaultAutofill.enable(); + } else { + this.autofillOnPageLoadForm.controls.defaultAutofill.disable(); + } } private async setAutofillKeyboardHelperText(command: string) { @@ -388,19 +488,6 @@ export class AutofillComponent implements OnInit { return await BrowserApi.permissionsGranted(["privacy"]); } - async updateContextMenuItem() { - await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); - this.messagingService.send("bgUpdateContextMenu"); - } - - async updateAutoTotpCopy() { - await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy); - } - - async saveClearClipboard() { - await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); - } - async updateShowCardsCurrentTab() { await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); } diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.html b/apps/browser/src/autofill/popup/settings/notifications.component.html index c6446012d0c..385db8c059b 100644 --- a/apps/browser/src/autofill/popup/settings/notifications.component.html +++ b/apps/browser/src/autofill/popup/settings/notifications.component.html @@ -47,7 +47,7 @@ - + {{ "excludedDomains" | i18n }} diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index f3047947c82..6bedb939c30 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -17,7 +17,6 @@ describe("InlineMenuFieldQualificationService", () => { fields: [], }); inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); - inlineMenuFieldQualificationService["inlineMenuFieldQualificationFlagSet"] = true; }); describe("isFieldForLoginForm", () => { diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 046381d956c..9b16a0cfbdd 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -150,7 +150,6 @@ export class InlineMenuFieldQualificationService this.identityPostalCodeAutocompleteValue, ]); private totpFieldAutocompleteValue = "one-time-code"; - private inlineMenuFieldQualificationFlagSet = false; private premiumEnabled = false; constructor() { @@ -158,7 +157,6 @@ export class InlineMenuFieldQualificationService sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"), sendExtensionMessage("getUserPremiumStatus"), ]).then(([fieldQualificationFlag, premiumStatus]) => { - this.inlineMenuFieldQualificationFlagSet = !!fieldQualificationFlag?.result; this.premiumEnabled = !!premiumStatus?.result; }); } @@ -170,10 +168,6 @@ export class InlineMenuFieldQualificationService * @param pageDetails - The details of the page that the field is on. */ isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean { - if (!this.inlineMenuFieldQualificationFlagSet) { - return this.isFieldForLoginFormFallback(field); - } - /** * Totp inline menu is available only for premium users. */ @@ -1223,18 +1217,4 @@ export class InlineMenuFieldQualificationService return false; } - - /** - * This method represents the previous rudimentary approach to qualifying fields for login forms. - * - * @param field - The field to validate - * @deprecated - This method will only be used when the fallback flag is set to true. - */ - private isFieldForLoginFormFallback(field: AutofillField): boolean { - if (field.type === "password") { - return true; - } - - return this.isUsernameField(field); - } } diff --git a/apps/browser/src/background/main.background.spec.ts b/apps/browser/src/background/main.background.spec.ts new file mode 100644 index 00000000000..c2cd38b7a30 --- /dev/null +++ b/apps/browser/src/background/main.background.spec.ts @@ -0,0 +1,13 @@ +// This test skips all the initilization of the background script and just +// focuses on making sure we don't accidently delete the initilization of +// background vault syncing. This has happened before! +describe("MainBackground sync task scheduling", () => { + it("includes code to schedule the sync interval task", () => { + // Get the bootstrap method source code as string + const { default: MainBackground } = jest.requireActual("./main.background"); + const bootstrapSource = MainBackground.prototype.bootstrap.toString(); + + // Check that the source includes the critical sync interval scheduling code + expect(bootstrapSource).toContain("this.backgroundSyncService.init();"); + }); +}); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 74fa6acdf79..5cc964c2c2d 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -26,8 +26,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; +import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -115,7 +115,6 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; -import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -128,7 +127,6 @@ import { WebPushNotificationsApiService, WorkerWebPushConnectionService, } from "@bitwarden/common/platform/notifications/internal"; -import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -223,6 +221,7 @@ import { KdfConfigService, KeyService as KeyServiceAbstraction, } from "@bitwarden/key-management"; +import { BackgroundSyncService } from "@bitwarden/platform/background-sync"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -392,6 +391,7 @@ export default class MainBackground { offscreenDocumentService: OffscreenDocumentService; syncServiceListener: SyncServiceListener; browserInitialInstallService: BrowserInitialInstallService; + backgroundSyncService: BackgroundSyncService; webPushConnectionService: WorkerWebPushConnectionService | UnsupportedWebPushConnectionService; themeStateService: DefaultThemeStateService; @@ -586,9 +586,9 @@ export default class MainBackground { this.logService, this.stateProvider, ); - this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () => - this.fullSync(), - ); + + this.backgroundSyncService = new BackgroundSyncService(this.taskSchedulerService); + this.backgroundSyncService.register(() => this.fullSync()); this.environmentService = new BrowserEnvironmentService( this.logService, @@ -655,9 +655,7 @@ export default class MainBackground { this.kdfConfigService, this.keyGenerationService, this.logService, - this.masterPasswordService, this.stateProvider, - this.stateService, ); this.keyService = new DefaultKeyService( @@ -674,19 +672,11 @@ export default class MainBackground { this.kdfConfigService, ); - this.biometricsService = new BackgroundBrowserBiometricsService( - runtimeNativeMessagingBackground, - this.logService, - this.keyService, - this.biometricStateService, - this.messagingService, - ); - this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.organizationService = new DefaultOrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); + this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new DefaultVaultTimeoutSettingsService( this.accountService, @@ -701,6 +691,15 @@ export default class MainBackground { VaultTimeoutStringType.OnRestart, // default vault timeout ); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + this.logService, + this.keyService, + this.biometricStateService, + this.messagingService, + this.vaultTimeoutSettingsService, + ); + this.apiService = new ApiService( this.tokenService, this.platformUtilsService, @@ -729,9 +728,14 @@ export default class MainBackground { this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, + this.accountService, ); this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); + this.policyApiService = new PolicyApiService( + this.policyService, + this.apiService, + this.accountService, + ); this.keyConnectorService = new KeyConnectorService( this.accountService, this.masterPasswordService, @@ -1026,6 +1030,7 @@ export default class MainBackground { this.cryptoFunctionService, this.kdfConfigService, this.accountService, + this.apiService, ); this.organizationVaultExportService = new OrganizationVaultExportService( @@ -1202,6 +1207,7 @@ export default class MainBackground { this.configService, this.platformUtilsService, this.policyService, + this.accountService, ); const contextMenuClickedHandler = new ContextMenuClickedHandler( @@ -1362,6 +1368,7 @@ export default class MainBackground { setTimeout(async () => { await this.refreshBadge(); await this.fullSync(false); + this.backgroundSyncService.init(); this.notificationsService.startListening(); resolve(); }, 500); @@ -1441,8 +1448,6 @@ export default class MainBackground { await this.popupViewCacheBackgroundService.clearState(); await this.accountService.switchAccount(userId); await switchPromise; - // Clear sequentialized caches - clearCaches(); if (userId == null) { await this.refreshBadge(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 8100ff3cffa..69521228bc5 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -120,9 +120,15 @@ export class NativeMessagingBackground { this.connecting = true; const connectedCallback = () => { - this.logService.info( - "[Native Messaging IPC] Connection to Bitwarden Desktop app established!", - ); + if (!this.platformUtilsService.isSafari()) { + this.logService.info( + "[Native Messaging IPC] Connection to Bitwarden Desktop app established!", + ); + } else { + this.logService.info( + "[Native Messaging IPC] Connection to Safari swift module established!", + ); + } this.connected = true; this.connecting = false; resolve(); @@ -131,6 +137,7 @@ export class NativeMessagingBackground { // Safari has a bundled native component which is always available, no need to // check if the desktop app is running. if (this.platformUtilsService.isSafari()) { + this.isConnectedToOutdatedDesktopClient = false; connectedCallback(); } @@ -428,7 +435,9 @@ export class NativeMessagingBackground { } if (this.callbacks.has(messageId)) { - this.callbacks.get(messageId)!.resolver(message); + const callback = this.callbacks!.get(messageId)!; + this.callbacks.delete(messageId); + callback.resolver(message); } else { this.logService.info("[Native Messaging IPC] Received message without a callback", message); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 7db72f38139..c1719abbb3a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -78,8 +78,8 @@ export default class RuntimeBackground { BiometricsCommands.GetBiometricsStatus, BiometricsCommands.UnlockWithBiometricsForUser, BiometricsCommands.GetBiometricsStatusForUser, + BiometricsCommands.CanEnableBiometricUnlock, "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", - "getInlineMenuFieldQualificationFeatureFlag", "getUserPremiumStatus", ]; @@ -201,14 +201,14 @@ export default class RuntimeBackground { case BiometricsCommands.GetBiometricsStatusForUser: { return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId); } + case BiometricsCommands.CanEnableBiometricUnlock: { + return await this.main.biometricsService.canEnableBiometricUnlock(); + } case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { return await this.configService.getFeatureFlag( FeatureFlag.UseTreeWalkerApiForPageDetailsCollection, ); } - case "getInlineMenuFieldQualificationFeatureFlag": { - return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification); - } case "getUserPremiumStatus": { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), diff --git a/apps/browser/src/billing/services/families-policy.service.spec.ts b/apps/browser/src/billing/services/families-policy.service.spec.ts index 65a861038bf..e9f75d52cb6 100644 --- a/apps/browser/src/billing/services/families-policy.service.spec.ts +++ b/apps/browser/src/billing/services/families-policy.service.spec.ts @@ -51,7 +51,7 @@ describe("FamiliesPolicyService", () => { organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: true }] as Policy[]; - policyService.getAll$.mockReturnValue(of(policies)); + policyService.policiesByType$.mockReturnValue(of(policies)); const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$()); expect(result).toBe(true); @@ -64,7 +64,7 @@ describe("FamiliesPolicyService", () => { organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: false }] as Policy[]; - policyService.getAll$.mockReturnValue(of(policies)); + policyService.policiesByType$.mockReturnValue(of(policies)); const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$()); expect(result).toBe(false); diff --git a/apps/browser/src/billing/services/families-policy.service.ts b/apps/browser/src/billing/services/families-policy.service.ts index 755d3e84591..42fa43cab1d 100644 --- a/apps/browser/src/billing/services/families-policy.service.ts +++ b/apps/browser/src/billing/services/families-policy.service.ts @@ -47,7 +47,7 @@ export class FamiliesPolicyService { map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), switchMap((enterpriseOrgId) => this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) + .policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) .pipe( map( (policies) => diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.spec.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.spec.ts new file mode 100644 index 00000000000..4017953ee28 --- /dev/null +++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.spec.ts @@ -0,0 +1,61 @@ +import { mock } from "jest-mock-extended"; + +import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; + +import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; + +import { BackgroundBrowserBiometricsService } from "./background-browser-biometrics.service"; + +describe("background browser biometrics service tests", function () { + let service: BackgroundBrowserBiometricsService; + + const nativeMessagingBackground = mock(); + const logService = mock(); + const keyService = mock(); + const biometricStateService = mock(); + const messagingService = mock(); + const vaultTimeoutSettingsService = mock(); + + beforeEach(() => { + jest.resetAllMocks(); + service = new BackgroundBrowserBiometricsService( + () => nativeMessagingBackground, + logService, + keyService, + biometricStateService, + messagingService, + vaultTimeoutSettingsService, + ); + }); + + describe("canEnableBiometricUnlock", () => { + const table: [BiometricsStatus, boolean, boolean][] = [ + // status, already enabled, expected + + // if the setting is not already on, it should only be possible to enable it if biometrics are available + [BiometricsStatus.Available, false, true], + [BiometricsStatus.HardwareUnavailable, false, false], + [BiometricsStatus.NotEnabledInConnectedDesktopApp, false, false], + [BiometricsStatus.DesktopDisconnected, false, false], + + // if the setting is already on, it should always be possible to disable it + [BiometricsStatus.Available, true, true], + [BiometricsStatus.HardwareUnavailable, true, true], + [BiometricsStatus.NotEnabledInConnectedDesktopApp, true, true], + [BiometricsStatus.DesktopDisconnected, true, true], + ]; + test.each(table)( + "status: %s, already enabled: %s, expected: %s", + async (status, alreadyEnabled, expected) => { + service.getBiometricsStatus = jest.fn().mockResolvedValue(status); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(alreadyEnabled); + const result = await service.canEnableBiometricUnlock(); + + expect(result).toBe(expected); + }, + ); + }); +}); diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts index 3031134dc34..a8a89d45274 100644 --- a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts +++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts @@ -1,5 +1,6 @@ import { Injectable } from "@angular/core"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -25,6 +26,7 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { private keyService: KeyService, private biometricStateService: BiometricStateService, private messagingService: MessagingService, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, ) { super(); } @@ -169,4 +171,14 @@ export class BackgroundBrowserBiometricsService extends BiometricsService { } async setShouldAutopromptNow(value: boolean): Promise {} + async canEnableBiometricUnlock(): Promise { + const status = await this.getBiometricsStatus(); + const isBiometricsAlreadyEnabled = await this.vaultTimeoutSettingsService.isBiometricLockSet(); + const statusAllowsBiometric = + status !== BiometricsStatus.DesktopDisconnected && + status !== BiometricsStatus.NotEnabledInConnectedDesktopApp && + status !== BiometricsStatus.HardwareUnavailable; + + return statusAllowsBiometric || isBiometricsAlreadyEnabled; + } } diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.spec.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.spec.ts new file mode 100644 index 00000000000..672eec8c1fc --- /dev/null +++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.spec.ts @@ -0,0 +1,60 @@ +import { mock } from "jest-mock-extended"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { BrowserApi } from "../../platform/browser/browser-api"; + +import { ForegroundBrowserBiometricsService } from "./foreground-browser-biometrics"; + +jest.mock("../../platform/browser/browser-api", () => ({ + BrowserApi: { + sendMessageWithResponse: jest.fn(), + permissionsGranted: jest.fn(), + }, +})); + +describe("foreground browser biometrics service tests", function () { + const platformUtilsService = mock(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe("canEnableBiometricUnlock", () => { + const table: [boolean, boolean, boolean, boolean][] = [ + // canEnableBiometricUnlock from background, native permission granted, isSafari, expected + + // needs permission prompt; always allowed + [true, false, false, true], + [false, false, false, true], + + // is safari; depends on the status that the background service reports + [false, false, true, false], + [true, false, true, true], + + // native permissions granted; depends on the status that the background service reports + [false, true, false, false], + [true, true, false, true], + + // should never happen since safari does not use the permissions + [false, true, true, false], + [true, true, true, true], + ]; + test.each(table)( + "canEnableBiometric: %s, native permission granted: %s, isSafari: %s, expected: %s", + async (canEnableBiometricUnlockBackground, granted, isSafari, expected) => { + const service = new ForegroundBrowserBiometricsService(platformUtilsService); + + (BrowserApi.permissionsGranted as jest.Mock).mockResolvedValue(granted); + (BrowserApi.sendMessageWithResponse as jest.Mock).mockResolvedValue({ + result: canEnableBiometricUnlockBackground, + }); + platformUtilsService.isSafari.mockReturnValue(isSafari); + + const result = await service.canEnableBiometricUnlock(); + + expect(result).toBe(expected); + }, + ); + }); +}); diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts index d248a630cc6..b6e84fee31a 100644 --- a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts +++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts @@ -1,3 +1,4 @@ +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -8,6 +9,10 @@ import { BrowserApi } from "../../platform/browser/browser-api"; export class ForegroundBrowserBiometricsService extends BiometricsService { shouldAutopromptNow = true; + constructor(private platformUtilsService: PlatformUtilsService) { + super(); + } + async authenticateWithBiometrics(): Promise { const response = await BrowserApi.sendMessageWithResponse<{ result: boolean; @@ -52,4 +57,19 @@ export class ForegroundBrowserBiometricsService extends BiometricsService { async setShouldAutopromptNow(value: boolean): Promise { this.shouldAutopromptNow = value; } + + async canEnableBiometricUnlock(): Promise { + const needsPermissionPrompt = + !(await BrowserApi.permissionsGranted(["nativeMessaging"])) && + !this.platformUtilsService.isSafari(); + return ( + needsPermissionPrompt || + ( + await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>(BiometricsCommands.CanEnableBiometricUnlock) + ).result + ); + } } diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 4510c2f342d..07aa3d2e4a9 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 2, "name": "__MSG_extName__", - "short_name": "__MSG_appName__", - "version": "2025.3.1", + "short_name": "Bitwarden", + "version": "2025.3.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index fc897c1b1c3..be1a3f17827 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -2,8 +2,8 @@ "manifest_version": 3, "minimum_chrome_version": "102.0", "name": "__MSG_extName__", - "short_name": "__MSG_appName__", - "version": "2025.3.1", + "short_name": "Bitwarden", + "version": "2025.3.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index 94f0846a852..2313b942a38 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -19,7 +19,7 @@ [ngClass]="{ 'tw-invisible': loading }" >
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index e73f56fa2f6..9948b450e17 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -7,7 +7,6 @@ import { EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { activeAuthGuard, authGuard, @@ -59,8 +58,6 @@ import { import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; -import { TwoFactorOptionsComponentV1 } from "../auth/popup/two-factor-options-v1.component"; -import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -142,32 +139,6 @@ const routes: Routes = [ canActivate: [fido2AuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...unauthUiRefreshSwap( - TwoFactorComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "2fa", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "2fa", - canActivate: [unauthGuardFn(unauthRouteOverrides), TwoFactorAuthGuard], - children: [ - { - path: "", - component: TwoFactorAuthComponent, - }, - ], - data: { - elevation: 1, - pageTitle: { - key: "verifyYourIdentity", - }, - showBackButton: true, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - }, - ), { path: "", component: ExtensionAnonLayoutWrapperComponent, @@ -191,12 +162,6 @@ const routes: Routes = [ }, ], }, - { - path: "2fa-options", - component: TwoFactorOptionsComponentV1, - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 } satisfies RouteDataProperties, - }, { path: "device-verification", component: ExtensionAnonLayoutWrapperComponent, @@ -371,7 +336,6 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - { path: "", component: ExtensionAnonLayoutWrapperComponent, @@ -567,6 +531,23 @@ const routes: Routes = [ }, ], }, + { + path: "2fa", + canActivate: [unauthGuardFn(unauthRouteOverrides), TwoFactorAuthGuard], + children: [ + { + path: "", + component: TwoFactorAuthComponent, + }, + ], + data: { + elevation: 1, + pageTitle: { + key: "verifyYourIdentity", + }, + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + }, ], }, { diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 80cffa03b17..b2542679e06 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -25,8 +25,6 @@ import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; -import { TwoFactorOptionsComponentV1 } from "../auth/popup/two-factor-options-v1.component"; -import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; @@ -93,8 +91,6 @@ import "../platform/popup/locales"; SetPasswordComponent, SsoComponentV1, TabsV2Component, - TwoFactorComponentV1, - TwoFactorOptionsComponentV1, UpdateTempPasswordComponent, UserVerificationComponent, VaultTimeoutInputComponent, diff --git a/apps/browser/src/popup/scss/misc.scss b/apps/browser/src/popup/scss/misc.scss index d1308d26180..8aace90d0a6 100644 --- a/apps/browser/src/popup/scss/misc.scss +++ b/apps/browser/src/popup/scss/misc.scss @@ -103,16 +103,6 @@ p.lead { margin: 0 !important; } -.no-vmargin { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.no-vpad { - padding-top: 0 !important; - padding-bottom: 0 !important; -} - .display-block { display: block !important; } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 86ab11a374a..26e84ea964c 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -316,10 +316,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: BiometricsService, - useFactory: () => { - return new ForegroundBrowserBiometricsService(); - }, - deps: [], + useClass: ForegroundBrowserBiometricsService, + deps: [PlatformUtilsService], }), safeProvider({ provide: SyncService, @@ -482,7 +480,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: AutofillSettingsServiceAbstraction, useClass: AutofillSettingsService, - deps: [StateProvider, PolicyService], + deps: [StateProvider, PolicyService, AccountService], }), safeProvider({ provide: UserNotificationSettingsServiceAbstraction, diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index d4ce360c32a..54e91611325 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -152,7 +152,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { response.userInfo = [ SFExtensionMessageKey: [ "message": [ - "command": "biometricUnlock", + "command": "unlockWithBiometricsForUser", "response": false, "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), "messageId": messageId, @@ -177,7 +177,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { response.userInfo = [ SFExtensionMessageKey: [ "message": [ - "command": "biometricUnlock", + "command": "unlockWithBiometricsForUser", "response": false, "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), "messageId": messageId, @@ -209,7 +209,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { response.userInfo = [ SFExtensionMessageKey: [ "message": [ - "command": "biometricUnlock", + "command": "unlockWithBiometricsForUser", "response": true, "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), "userKeyB64": result!.replacingOccurrences(of: "\"", with: ""), @@ -220,7 +220,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { response.userInfo = [ SFExtensionMessageKey: [ "message": [ - "command": "biometricUnlock", + "command": "unlockWithBiometricsForUser", "response": true, "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), "messageId": messageId, diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index c3f4634a6c2..6fc4793f5c0 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -64,7 +64,7 @@ describe("SendV2Component", () => { }); policyService = mock(); - policyService.policyAppliesToActiveUser$.mockReturnValue(of(true)); // Return `true` by default + policyService.policyAppliesToUser$.mockReturnValue(of(true)); // Return `true` by default sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index 2766ba56c95..49804abda5d 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -2,11 +2,13 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; -import { combineLatest } from "rxjs"; +import { combineLatest, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components"; import { @@ -66,6 +68,7 @@ export class SendV2Component implements OnInit, OnDestroy { protected sendItemsService: SendItemsService, protected sendListFiltersService: SendListFiltersService, private policyService: PolicyService, + private accountService: AccountService, ) { combineLatest([ this.sendItemsService.emptyList$, @@ -93,9 +96,14 @@ export class SendV2Component implements OnInit, OnDestroy { this.listState = null; }); - this.policyService - .policyAppliesToActiveUser$(PolicyType.DisableSend) - .pipe(takeUntilDestroyed()) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId), + ), + takeUntilDestroyed(), + ) .subscribe((sendsDisabled) => { this.sendsDisabled = sendsDisabled; }); diff --git a/apps/browser/src/vault/guards/at-risk-passwords.guard.ts b/apps/browser/src/vault/guards/at-risk-passwords.guard.ts index ee991c81239..6bcdddfde81 100644 --- a/apps/browser/src/vault/guards/at-risk-passwords.guard.ts +++ b/apps/browser/src/vault/guards/at-risk-passwords.guard.ts @@ -4,8 +4,9 @@ import { switchMap, tap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { TaskService } from "@bitwarden/common/vault/tasks"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { ToastService } from "@bitwarden/components"; -import { filterOutNullish, TaskService } from "@bitwarden/vault"; export const canAccessAtRiskPasswords: CanActivateFn = () => { const accountService = inject(AccountService); diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts index 5e46f3cd3d9..eb5cd459111 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts @@ -4,9 +4,10 @@ import { RouterModule } from "@angular/router"; import { map, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { AnchorLinkDirective, CalloutModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { filterOutNullish, SecurityTaskType, TaskService } from "@bitwarden/vault"; // TODO: This component will need to be reworked to use the new EndUserNotificationService in PM-10609 diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html index aee456a8f2b..e63b06750c8 100644 --- a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html @@ -6,7 +6,7 @@ class="tw-max-w-full tw-max-h-40" src="../../../../images/at-risk-password-carousel/review_at-risk_logins.light.png" appDarkImgSrc="../../../../images/at-risk-password-carousel/review_at-risk_logins.dark.png" - [alt]="'reviewAtRiskLoginSlideImgAlt' | i18n" + [alt]="'reviewAtRiskLoginSlideImgAltPeriod' | i18n" />

{{ "reviewAtRiskLogins" | i18n }}

@@ -18,7 +18,7 @@ class="tw-max-w-full tw-max-h-40" src="../../../../images/at-risk-password-carousel/generate_password.light.png" appDarkImgSrc="../../../../images/at-risk-password-carousel/generate_password.dark.png" - [alt]="'generatePasswordSlideImgAlt' | i18n" + [alt]="'generatePasswordSlideImgAltPeriod' | i18n" />

{{ "generatePassword" | i18n }}

@@ -30,7 +30,7 @@ class="tw-max-w-full tw-max-h-40" src="../../../../images/at-risk-password-carousel/update_login.light.png" appDarkImgSrc="../../../../images/at-risk-password-carousel/update_login.dark.png" - [alt]="'updateInBitwardenSlideImgAlt' | i18n" + [alt]="'updateInBitwardenSlideImgAltPeriod' | i18n" />

{{ "updateInBitwarden" | i18n }}

@@ -39,7 +39,7 @@

-
+ -
+ diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts index 3bf786ad5b7..25bf3ce3716 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts @@ -16,14 +16,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; import { ChangeLoginPasswordService, DefaultChangeLoginPasswordService, PasswordRepromptService, - SecurityTask, - SecurityTaskType, - TaskService, } from "@bitwarden/vault"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index dd3d53fed7d..6711673751c 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -15,6 +15,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { BadgeModule, ButtonModule, @@ -28,10 +30,7 @@ import { import { ChangeLoginPasswordService, DefaultChangeLoginPasswordService, - filterOutNullish, PasswordRepromptService, - SecurityTaskType, - TaskService, VaultCarouselModule, } from "@bitwarden/vault"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 40f00ab4332..19f1668eba4 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -7,6 +7,6 @@ [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null" showAutofillButton [disableDescriptionMargin]="showEmptyAutofillTip$ | async" - [primaryActionAutofill]="clickItemsToAutofillVaultView" + [primaryActionAutofill]="clickItemsToAutofillVaultView$ | async" [groupByType]="groupByType()" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index 03d84120785..72d51776f7b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; -import { combineLatest, firstValueFrom, map, Observable } from "rxjs"; +import { combineLatest, map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -33,7 +33,7 @@ import { VaultListItemsContainerComponent } from "../vault-list-items-container/ selector: "app-autofill-vault-list-items", templateUrl: "autofill-vault-list-items.component.html", }) -export class AutofillVaultListItemsComponent implements OnInit { +export class AutofillVaultListItemsComponent { /** * The list of ciphers that can be used to autofill the current page. * @protected @@ -47,7 +47,9 @@ export class AutofillVaultListItemsComponent implements OnInit { */ protected showRefresh: boolean = BrowserPopupUtils.inSidebar(window); - clickItemsToAutofillVaultView = false; + /** Flag indicating whether the login item should automatically autofill when clicked */ + protected clickItemsToAutofillVaultView$: Observable = + this.vaultSettingsService.clickItemsToAutofillVaultView$; protected groupByType = toSignal( this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)), @@ -84,12 +86,6 @@ export class AutofillVaultListItemsComponent implements OnInit { // TODO: Migrate logic to show Autofill policy toast PM-8144 } - async ngOnInit() { - this.clickItemsToAutofillVaultView = await firstValueFrom( - this.vaultSettingsService.clickItemsToAutofillVaultView$, - ); - } - /** * Refreshes the current tab to re-populate the autofill ciphers. * @protected diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index cbce4bf2961..a55bba622e4 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -99,7 +99,9 @@ type="button" (click)="primaryActionOnSelect(cipher)" (dblclick)="launchCipher(cipher)" - [appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name" + [appA11yTitle]=" + cipherItemTitleKey(cipher) | async | i18n: cipher.name : cipher.login.username + " class="{{ itemHeightClass }}" >
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 9d70c0ba236..6df1bdf8ae5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -206,11 +206,14 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { /** * Resolved i18n key to use for suggested cipher items */ - cipherItemTitleKey = this.currentURIIsBlocked$.pipe( - map((uriIsBlocked) => - this.primaryActionAutofill && !uriIsBlocked ? "autofillTitle" : "viewItemTitle", - ), - ); + cipherItemTitleKey = (cipher: CipherView) => + this.currentURIIsBlocked$.pipe( + map((uriIsBlocked) => { + const hasUsername = cipher.login?.username != null; + const key = this.primaryActionAutofill && !uriIsBlocked ? "autofillTitle" : "viewItemTitle"; + return hasUsername ? `${key}WithField` : key; + }), + ); /** * Option to show the autofill button for each item. diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 92276ef633f..f8bbb930f72 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -170,7 +170,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { this.cipherService .failedToDecryptCiphers$(activeUserId) .pipe( - map((ciphers) => ciphers.filter((c) => !c.isDeleted)), + map((ciphers) => (ciphers ? ciphers.filter((c) => !c.isDeleted) : [])), filter((ciphers) => ciphers.length > 0), take(1), takeUntilDestroyed(this.destroyRef), diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index b9eae380ca0..6b64e1191fc 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -33,19 +33,17 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip import { AsyncActionsModule, ButtonModule, + CalloutModule, DialogService, IconButtonModule, SearchModule, ToastService, - CalloutModule, } from "@bitwarden/components"; import { ChangeLoginPasswordService, CipherViewComponent, CopyCipherFieldService, DefaultChangeLoginPasswordService, - DefaultTaskService, - TaskService, } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; @@ -95,7 +93,6 @@ type LoadAction = providers: [ { provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, - { provide: TaskService, useClass: DefaultTaskService }, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], }) diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index dac6a141d41..5f2ec858ed6 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -108,7 +108,7 @@ export class VaultPopupItemsService { this.cipherService.failedToDecryptCiphers$(userId), ]), ), - map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]), + map(([ciphers, failedToDecryptCiphers]) => [...(failedToDecryptCiphers || []), ...ciphers]), ), ), shareReplay({ refCount: true, bufferSize: 1 }), diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 99a27c54bcc..f9785bccd00 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -36,7 +36,7 @@ describe("VaultPopupListFiltersService", () => { let folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); let decryptedCollections$ = new BehaviorSubject([]); - const policyAppliesToActiveUser$ = new BehaviorSubject(false); + const policyAppliesToUser$ = new BehaviorSubject(false); let viewCacheService: { signal: jest.Mock; mockSignal: WritableSignal; @@ -65,7 +65,7 @@ describe("VaultPopupListFiltersService", () => { } as I18nService; const policyService = { - policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$), + policyAppliesToUser$: jest.fn(() => policyAppliesToUser$), }; const state$ = new BehaviorSubject(false); @@ -75,8 +75,8 @@ describe("VaultPopupListFiltersService", () => { _memberOrganizations$ = new BehaviorSubject([]); // Fresh instance per test folderViews$ = new BehaviorSubject([]); // Fresh instance per test decryptedCollections$ = new BehaviorSubject([]); // Fresh instance per test - policyAppliesToActiveUser$.next(false); - policyService.policyAppliesToActiveUser$.mockClear(); + policyAppliesToUser$.next(false); + policyService.policyAppliesToUser$.mockClear(); const accountService = mockAccountServiceWith("userId" as UserId); const mockCachedSignal = createMockSignal({}); @@ -196,14 +196,15 @@ describe("VaultPopupListFiltersService", () => { }); describe("PersonalOwnership policy", () => { - it('calls policyAppliesToActiveUser$ with "PersonalOwnership"', () => { - expect(policyService.policyAppliesToActiveUser$).toHaveBeenCalledWith( + it('calls policyAppliesToUser$ with "PersonalOwnership"', () => { + expect(policyService.policyAppliesToUser$).toHaveBeenCalledWith( PolicyType.PersonalOwnership, + "userId", ); }); it("returns an empty array when the policy applies and there is a single organization", (done) => { - policyAppliesToActiveUser$.next(true); + policyAppliesToUser$.next(true); _memberOrganizations$.next([ { name: "bobby's org", id: "1234-3323-23223" }, ] as Organization[]); @@ -215,7 +216,7 @@ describe("VaultPopupListFiltersService", () => { }); it('adds "myVault" when the policy does not apply and there are multiple organizations', (done) => { - policyAppliesToActiveUser$.next(false); + policyAppliesToUser$.next(false); const orgs = [ { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-4343-99888" }, @@ -234,7 +235,7 @@ describe("VaultPopupListFiltersService", () => { }); it('does not add "myVault" the policy applies and there are multiple organizations', (done) => { - policyAppliesToActiveUser$.next(true); + policyAppliesToUser$.next(true); const orgs = [ { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-3242-99888" }, @@ -679,7 +680,7 @@ function createSeededVaultPopupListFiltersService( } as any; const policyServiceMock = { - policyAppliesToActiveUser$: jest.fn(() => new BehaviorSubject(false)), + policyAppliesToUser$: jest.fn(() => new BehaviorSubject(false)), } as any; const stateProviderMock = { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 8d9f6664e45..6cce5796cbe 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -8,12 +8,10 @@ import { filter, map, Observable, - of, shareReplay, startWith, switchMap, take, - tap, } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; @@ -23,6 +21,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -288,68 +287,70 @@ export class VaultPopupListFiltersService { /** * Organization array structured to be directly passed to `ChipSelectComponent` */ - organizations$: Observable[]> = combineLatest([ + + organizations$: Observable[]> = this.accountService.activeAccount$.pipe( - switchMap((account) => - account === null ? of([]) : this.organizationService.memberOrganizations$(account.id), + getUserId, + switchMap((userId) => + combineLatest([ + this.organizationService.memberOrganizations$(userId), + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ]), ), - ), - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ]).pipe( - map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ - orgs.sort(Utils.getSortFunction(this.i18nService, "name")), - personalOwnershipApplies, - ]), - map(([orgs, personalOwnershipApplies]) => { - // When there are no organizations return an empty array, - // resulting in the org filter being hidden - if (!orgs.length) { - return []; - } + map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ + orgs.sort(Utils.getSortFunction(this.i18nService, "name")), + personalOwnershipApplies, + ]), + map(([orgs, personalOwnershipApplies]) => { + // When there are no organizations return an empty array, + // resulting in the org filter being hidden + if (!orgs.length) { + return []; + } - // When there is only one organization and personal ownership policy applies, - // return an empty array, resulting in the org filter being hidden - if (orgs.length === 1 && personalOwnershipApplies) { - return []; - } + // When there is only one organization and personal ownership policy applies, + // return an empty array, resulting in the org filter being hidden + if (orgs.length === 1 && personalOwnershipApplies) { + return []; + } - const myVaultOrg: ChipSelectOption[] = []; + const myVaultOrg: ChipSelectOption[] = []; - // Only add "My vault" if personal ownership policy does not apply - if (!personalOwnershipApplies) { - myVaultOrg.push({ - value: { id: MY_VAULT_ID } as Organization, - label: this.i18nService.t("myVault"), - icon: "bwi-user", - }); - } + // Only add "My vault" if personal ownership policy does not apply + if (!personalOwnershipApplies) { + myVaultOrg.push({ + value: { id: MY_VAULT_ID } as Organization, + label: this.i18nService.t("myVault"), + icon: "bwi-user", + }); + } - return [ - ...myVaultOrg, - ...orgs.map((org) => { - let icon = "bwi-business"; + return [ + ...myVaultOrg, + ...orgs.map((org) => { + let icon = "bwi-business"; - if (!org.enabled) { - // Show a warning icon if the organization is deactivated - icon = "bwi-exclamation-triangle tw-text-danger"; - } else if ( - org.productTierType === ProductTierType.Families || - org.productTierType === ProductTierType.Free - ) { - // Show a family icon if the organization is a family or free org - icon = "bwi-family"; - } + if (!org.enabled) { + // Show a warning icon if the organization is deactivated + icon = "bwi-exclamation-triangle tw-text-danger"; + } else if ( + org.productTierType === ProductTierType.Families || + org.productTierType === ProductTierType.Free + ) { + // Show a family icon if the organization is a family or free org + icon = "bwi-family"; + } - return { - value: org, - label: org.name, - icon, - }; - }), - ]; - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); + return { + value: org, + label: org.name, + icon, + }; + }), + ]; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); /** * Folder array structured to be directly passed to `ChipSelectComponent` @@ -358,10 +359,10 @@ export class VaultPopupListFiltersService { switchMap((userId) => { // Observable of cipher views const cipherViews$ = this.cipherService.cipherViews$(userId).pipe( - tap((cipherViews) => { - this.cipherViews = Object.values(cipherViews); + map((ciphers) => { + this.cipherViews = ciphers ? Object.values(ciphers) : []; + return this.cipherViews; }), - map((ciphers) => Object.values(ciphers)), ); return combineLatest([ diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index e836af1327c..e24985f58af 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -29,6 +29,7 @@ "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"], "@bitwarden/platform": ["../../libs/platform/src"], + "@bitwarden/platform/*": ["../../libs/platform/src/*"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/ui-common": ["../../libs/ui/common/src"], diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 9af6e1f0613..107afc6dc8d 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -5,7 +5,7 @@ import * as http from "http"; import { OptionValues } from "commander"; import * as inquirer from "inquirer"; import Separator from "inquirer/lib/objects/separator"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { LoginStrategyServiceAbstraction, @@ -29,6 +29,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -555,7 +556,10 @@ export class LoginCommand { ); const enforcedPolicyOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + ), ); // Verify master password meets policy requirements diff --git a/apps/cli/src/key-management/cli-biometrics-service.ts b/apps/cli/src/key-management/cli-biometrics-service.ts index bda8fe82895..b4f802eb053 100644 --- a/apps/cli/src/key-management/cli-biometrics-service.ts +++ b/apps/cli/src/key-management/cli-biometrics-service.ts @@ -24,4 +24,7 @@ export class CliBiometricsService extends BiometricsService { } async setShouldAutopromptNow(value: boolean): Promise {} + async canEnableBiometricUnlock(): Promise { + return false; + } } diff --git a/apps/cli/src/platform/services/lowdb-storage.service.ts b/apps/cli/src/platform/services/lowdb-storage.service.ts index f4542236395..61d3920c918 100644 --- a/apps/cli/src/platform/services/lowdb-storage.service.ts +++ b/apps/cli/src/platform/services/lowdb-storage.service.ts @@ -14,7 +14,6 @@ import { AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; -import { sequentialize } from "@bitwarden/common/platform/misc/sequentialize"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { NodeUtils } from "@bitwarden/node/node-utils"; @@ -44,7 +43,6 @@ export class LowdbStorageService implements AbstractStorageService { this.updates$ = this.updatesSubject.asObservable(); } - @sequentialize(() => "lowdbStorageInit") async init() { if (this.ready) { return; diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index b7f423e8ff7..6a4651bcd5a 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -28,8 +28,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; +import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -237,7 +237,7 @@ export class ServiceContainer { cryptoFunctionService: NodeCryptoFunctionService; encryptService: EncryptServiceImplementation; authService: AuthService; - policyService: PolicyService; + policyService: DefaultPolicyService; policyApiService: PolicyApiServiceAbstraction; logService: ConsoleLogService; sendService: SendService; @@ -436,9 +436,7 @@ export class ServiceContainer { this.kdfConfigService, this.keyGenerationService, this.logService, - this.masterPasswordService, this.stateProvider, - this.stateService, ); this.keyService = new KeyService( @@ -469,7 +467,7 @@ export class ServiceContainer { this.ssoUrlService = new SsoUrlService(); this.organizationService = new DefaultOrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); + this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new DefaultVaultTimeoutSettingsService( this.accountService, @@ -560,7 +558,11 @@ export class ServiceContainer { this.providerService = new ProviderService(this.stateProvider); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); + this.policyApiService = new PolicyApiService( + this.policyService, + this.apiService, + this.accountService, + ); this.keyConnectorService = new KeyConnectorService( this.accountService, @@ -672,6 +674,7 @@ export class ServiceContainer { this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, + this.accountService, ); this.cipherService = new CipherService( @@ -795,6 +798,7 @@ export class ServiceContainer { this.cryptoFunctionService, this.kdfConfigService, this.accountService, + this.apiService, ); this.organizationExportService = new OrganizationVaultExportService( diff --git a/apps/cli/src/tools/export.command.ts b/apps/cli/src/tools/export.command.ts index b2d9d0151a9..f5fea794eef 100644 --- a/apps/cli/src/tools/export.command.ts +++ b/apps/cli/src/tools/export.command.ts @@ -2,16 +2,23 @@ // @ts-strict-ignore import { OptionValues } from "commander"; import * as inquirer from "inquirer"; +import { firstValueFrom, switchMap } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ExportFormat, EXPORT_FORMATS, VaultExportServiceAbstraction, + ExportedVault, + ExportedVaultAsBlob, } from "@bitwarden/vault-export-core"; import { Response } from "../models/response"; @@ -22,13 +29,19 @@ export class ExportCommand { private exportService: VaultExportServiceAbstraction, private policyService: PolicyService, private eventCollectionService: EventCollectionService, + private accountService: AccountService, + private configService: ConfigService, ) {} async run(options: OptionValues): Promise { - if ( - options.organizationid == null && - (await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport)) - ) { + const policyApplies$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisablePersonalVaultExport, userId), + ), + ); + + if (options.organizationid == null && (await firstValueFrom(policyApplies$))) { return Response.badRequest( "One or more organization policies prevents you from exporting your personal vault.", ); @@ -42,6 +55,13 @@ export class ExportCommand { const format = password && options.format == "json" ? "encrypted_json" : (options.format ?? "csv"); + if ( + format == "zip" && + !(await this.configService.getFeatureFlag(FeatureFlag.ExportAttachments)) + ) { + return Response.badRequest("Exporting attachments is not supported in this environment."); + } + if (!this.isSupportedExportFormat(format)) { return Response.badRequest( `'${format}' is not a supported export format. Supported formats: ${EXPORT_FORMATS.join( @@ -54,7 +74,7 @@ export class ExportCommand { return Response.error("`" + options.organizationid + "` is not a GUID."); } - let exportContent: string = null; + let exportContent: ExportedVault = null; try { if (format === "encrypted_json") { password = await this.promptPassword(password); @@ -78,34 +98,28 @@ export class ExportCommand { } catch (e) { return Response.error(e); } - return await this.saveFile(exportContent, options, format); + return await this.saveFile(exportContent, options); } - private async saveFile( - exportContent: string, - options: OptionValues, - format: ExportFormat, - ): Promise { + private async saveFile(exportContent: ExportedVault, options: OptionValues): Promise { try { - const fileName = this.getFileName(format, options.organizationid != null ? "org" : null); - return await CliUtils.saveResultToFile(exportContent, options.output, fileName); + if (exportContent.type === "application/zip") { + exportContent = exportContent as ExportedVaultAsBlob; + const arrayBuffer = await exportContent.data.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + return await CliUtils.saveResultToFile(buffer, options.output, exportContent.fileName); + } + + return await CliUtils.saveResultToFile( + exportContent.data, + options.output, + exportContent.fileName, + ); } catch (e) { return Response.error(e.toString()); } } - private getFileName(format: ExportFormat, prefix?: string) { - if (format === "encrypted_json") { - if (prefix == null) { - prefix = "encrypted"; - } else { - prefix = "encrypted_" + prefix; - } - format = "json"; - } - return this.exportService.getFileName(prefix, format); - } - private async promptPassword(password: string | boolean) { // boolean => flag set with no value, we need to prompt for password // string => flag set with value, use this value for password diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index d6ef27b1428..81816540d12 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -501,6 +501,8 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.exportService, this.serviceContainer.policyService, this.serviceContainer.eventCollectionService, + this.serviceContainer.accountService, + this.serviceContainer.configService, ); const response = await command.run(options); this.processResponse(response); diff --git a/apps/desktop/src/app/accounts/settings.component.spec.ts b/apps/desktop/src/app/accounts/settings.component.spec.ts index 342d4717511..b05d90b7e1c 100644 --- a/apps/desktop/src/app/accounts/settings.component.spec.ts +++ b/apps/desktop/src/app/accounts/settings.component.spec.ts @@ -153,7 +153,7 @@ describe("SettingsComponent", () => { it("pin enabled when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); @@ -164,7 +164,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -175,7 +175,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -184,7 +184,7 @@ describe("SettingsComponent", () => { it("pin visible when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); fixture.detectChanges(); @@ -201,7 +201,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); @@ -218,7 +218,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); pinServiceAbstraction.isPinSet.mockResolvedValue(true); await component.ngOnInit(); @@ -236,7 +236,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); @@ -248,6 +248,7 @@ describe("SettingsComponent", () => { describe("biometrics enabled", () => { beforeEach(() => { desktopBiometricsService.getBiometricsStatus.mockResolvedValue(BiometricsStatus.Available); + desktopBiometricsService.canEnableBiometricUnlock.mockResolvedValue(true); vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(true); }); @@ -255,7 +256,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); i18nService.t.mockImplementation((id: string) => { if (id === "requirePasswordOnStart") { @@ -290,7 +291,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); i18nService.t.mockImplementation((id: string) => { if (id === "requirePasswordOnStart") { diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index cec0d98ccac..a592e542d58 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -17,8 +17,10 @@ import { import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DeviceType } from "@bitwarden/common/enums"; @@ -235,7 +237,12 @@ export class SettingsComponent implements OnInit, OnDestroy { ); // Load timeout policy - this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe( + this.vaultTimeoutPolicyCallout = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), + ), + getFirstPolicy, filter((policy) => policy != null), map((policy) => { let timeout; @@ -259,7 +266,12 @@ export class SettingsComponent implements OnInit, OnDestroy { // Load initial values this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id); - this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe( + this.pinEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId), + ), + getFirstPolicy, map((policy) => { return policy == null || !policy.enabled; }), @@ -376,24 +388,12 @@ export class SettingsComponent implements OnInit, OnDestroy { } }); - this.supportsBiometric = this.shouldAllowBiometricSetup( - await this.biometricsService.getBiometricsStatus(), - ); + this.supportsBiometric = await this.biometricsService.canEnableBiometricUnlock(); this.timerId = setInterval(async () => { - this.supportsBiometric = this.shouldAllowBiometricSetup( - await this.biometricsService.getBiometricsStatus(), - ); + this.supportsBiometric = await this.biometricsService.canEnableBiometricUnlock(); }, 1000); } - private shouldAllowBiometricSetup(biometricStatus: BiometricsStatus): boolean { - return [ - BiometricsStatus.Available, - BiometricsStatus.AutoSetupNeeded, - BiometricsStatus.ManualSetupNeeded, - ].includes(biometricStatus); - } - async saveVaultTimeout(newValue: VaultTimeout) { if (newValue === VaultTimeoutStringType.Never) { const confirmed = await this.dialogService.openSimpleDialog({ diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 3bb130d321d..cd5064a87e4 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -6,7 +6,6 @@ import { DesktopDefaultOverlayPosition, EnvironmentSelectorComponent, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, @@ -53,7 +52,6 @@ import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.compo import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; -import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; @@ -76,28 +74,6 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })], }, - ...unauthUiRefreshSwap( - TwoFactorComponentV1, - AnonLayoutWrapperComponent, - { - path: "2fa", - }, - { - path: "2fa", - canActivate: [unauthGuardFn(), TwoFactorAuthGuard], - children: [ - { - path: "", - component: TwoFactorAuthComponent, - }, - ], - data: { - pageTitle: { - key: "verifyYourIdentity", - }, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - }, - ), { path: "authentication-timeout", component: AnonLayoutWrapperComponent, @@ -360,6 +336,21 @@ const routes: Routes = [ }, } satisfies AnonLayoutWrapperData, }, + { + path: "2fa", + canActivate: [unauthGuardFn(), TwoFactorAuthGuard], + children: [ + { + path: "", + component: TwoFactorAuthComponent, + }, + ], + data: { + pageTitle: { + key: "verifyYourIdentity", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, ], }, ]; diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 924bc2dd30f..e0075975900 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -49,7 +49,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; -import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; @@ -406,8 +405,6 @@ export class AppComponent implements OnInit, OnDestroy { this.router.navigate(["/remove-password"]); break; case "switchAccount": { - // Clear sequentialized caches - clearCaches(); if (message.userId != null) { await this.accountService.switchAccount(message.userId); } diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index b1b2864af5a..b717afe4a41 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -16,8 +16,6 @@ import { LoginModule } from "../auth/login/login.module"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { SsoComponentV1 } from "../auth/sso-v1.component"; -import { TwoFactorOptionsComponentV1 } from "../auth/two-factor-options-v1.component"; -import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { SshAgentService } from "../autofill/services/ssh-agent.service"; import { PremiumComponent } from "../billing/app/accounts/premium.component"; @@ -78,9 +76,7 @@ import { SharedModule } from "./shared/shared.module"; SetPasswordComponent, SettingsComponent, ShareComponent, - TwoFactorComponentV1, SsoComponentV1, - TwoFactorOptionsComponentV1, UpdateTempPasswordComponent, VaultComponent, VaultTimeoutInputComponent, diff --git a/apps/desktop/src/app/tools/send/add-edit.component.html b/apps/desktop/src/app/tools/send/add-edit.component.html index 3d8231d07ce..0bf2e1778e0 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.html +++ b/apps/desktop/src/app/tools/send/add-edit.component.html @@ -282,14 +282,9 @@