diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c4202ed2a68..ee97f16b0a9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -49,6 +49,7 @@ "./github/workflows/release-web.yml", ], commitMessagePrefix: "[deps] BRE:", + addLabels: ["hold"], }, { // Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular. diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 365d29f17f7..86dc74f7351 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -92,6 +92,7 @@ jobs: id: retrieve-version run: | PKG_VERSION=$(jq -r .version src/package.json) + echo "Setting version number to $PKG_VERSION" echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT - name: Increment Version @@ -725,6 +726,11 @@ jobs: --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ --output none + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --output none + - name: Get certificates if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | @@ -784,6 +790,15 @@ jobs: cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile + mkdir -p $HOME/Library/MobileDevice/Provisioning\ Profiles + export APP_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_appstore.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + export AUTOFILL_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + + cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ + $HOME/Library/MobileDevice/Provisioning\ Profiles/$APP_UUID.provisionprofile + cp $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + $HOME/Library/MobileDevice/Provisioning\ Profiles/$AUTOFILL_UUID.provisionprofile + - name: Increment version shell: pwsh env: @@ -914,8 +929,13 @@ jobs: mkdir -p $HOME/secrets az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ - --name bitwarden_desktop_appstore.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ + --name bitwarden_desktop_developer_id.provisionprofile \ + --file $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile \ + --output none + + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name bitwarden_desktop_autofill_developer_id.provisionprofile \ + --file $HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile \ --output none - name: Get certificates @@ -958,21 +978,21 @@ jobs: security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security import "$HOME/certificates/appstore-app-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - name: Set up provisioning profiles run: | - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile + cp $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile \ + $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_developer_id.provisionprofile + + mkdir -p $HOME/Library/MobileDevice/Provisioning\ Profiles + export APP_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + export AUTOFILL_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + + cp $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile \ + $HOME/Library/MobileDevice/Provisioning\ Profiles/$APP_UUID.provisionprofile + cp $HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile \ + $HOME/Library/MobileDevice/Provisioning\ Profiles/$AUTOFILL_UUID.provisionprofile - name: Increment version shell: pwsh @@ -1020,7 +1040,9 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' working-directory: apps/desktop/desktop_native - run: node build.js cross-platform + run: | + rustup target add aarch64-apple-darwin + node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1167,6 +1189,11 @@ jobs: --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ --output none + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --output none + - name: Get certificates run: | mkdir -p $HOME/certificates @@ -1201,21 +1228,12 @@ jobs: security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security import "$HOME/certificates/appstore-app-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - name: Set up provisioning profiles @@ -1223,6 +1241,15 @@ jobs: cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile + mkdir -p $HOME/Library/MobileDevice/Provisioning\ Profiles + export APP_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_appstore.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + export AUTOFILL_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + + cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ + $HOME/Library/MobileDevice/Provisioning\ Profiles/$APP_UUID.provisionprofile + cp $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + $HOME/Library/MobileDevice/Provisioning\ Profiles/$AUTOFILL_UUID.provisionprofile + - name: Increment version shell: pwsh env: @@ -1269,7 +1296,9 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' working-directory: apps/desktop/desktop_native - run: node build.js cross-platform + run: | + rustup target add aarch64-apple-darwin + node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1378,226 +1407,6 @@ jobs: env: BUILD_NUMBER: ${{ needs.setup.outputs.build_number }} - - macos-package-dev: - name: MacOS Package Dev Release Asset - runs-on: macos-13 - if: ${{ needs.setup.outputs.has_secrets == 'true' }} - needs: - - browser-build - - macos-build - - setup - env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} - _NODE_VERSION: ${{ needs.setup.outputs.node_version }} - NODE_OPTIONS: --max_old_space_size=4096 - defaults: - run: - working-directory: apps/desktop - steps: - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Set up Node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - with: - cache: 'npm' - cache-dependency-path: '**/package-lock.json' - node-version: ${{ env._NODE_VERSION }} - - - name: Set up Node-gyp - run: python3 -m pip install setuptools - - - name: Print environment - run: | - node --version - npm --version - echo "GitHub ref: $GITHUB_REF" - echo "GitHub event: $GITHUB_EVENT" - - - name: Get Build Cache - id: build-cache - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: apps/desktop/build - key: ${{ runner.os }}-${{ github.run_id }}-build - - - name: Setup Safari Cache - id: safari-cache - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: apps/browser/dist/Safari - key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Download Provisioning Profiles secrets - env: - ACCOUNT_NAME: bitwardenci - CONTAINER_NAME: profiles - run: | - mkdir -p $HOME/secrets - - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ - --name bitwarden_desktop_appstore.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - --output none - - - name: Get certificates - run: | - mkdir -p $HOME/certificates - - az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | - jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - - az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 - - az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-installer-cert.p12 - - az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 - - az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12 - - az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | - jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 - - - name: Set up keychain - env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - run: | - security create-keychain -p $KEYCHAIN_PASSWORD build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain - security set-keychain-settings -lut 1200 build.keychain - - security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/appstore-app-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \ - -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - - - name: Set up provisioning profiles - run: | - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile - - - name: Increment version - shell: pwsh - env: - BUILD_NUMBER: ${{ needs.setup.outputs.build_number }} - run: | - $package = Get-Content -Raw -Path electron-builder.json | ConvertFrom-Json - $package | Add-Member -MemberType NoteProperty -Name buildVersion -Value "$env:BUILD_NUMBER" - $package | ConvertTo-Json -Depth 32 | Set-Content -Path electron-builder.json - Write-Output "### MacOS Dev build number: $env:BUILD_NUMBER" - - - name: Install Node dependencies - run: npm ci - working-directory: ./ - - - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - workflow: build-wasm-internal.yml - workflow_conclusion: success - branch: ${{ inputs.sdk_branch }} - artifacts: sdk-internal - repo: bitwarden/sdk-internal - path: ../sdk-internal - if_no_artifact_found: fail - - - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} - working-directory: ./ - run: | - ls -l ../ - npm link ../sdk-internal - - - name: Cache Native Module - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: cache - with: - path: | - apps/desktop/desktop_native/napi/*.node - apps/desktop/desktop_native/dist/* - key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - - - name: Build Native Module - if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native - run: node build.js cross-platform - - - name: Build - if: steps.build-cache.outputs.cache-hit != 'true' - run: npm run build - - - name: Download Browser artifact - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - path: ${{ github.workspace }}/browser-build-artifacts - - - name: Unzip Safari artifact - run: | - SAFARI_DIR=$(find $GITHUB_WORKSPACE/browser-build-artifacts -name 'dist-safari-*.zip') - echo $SAFARI_DIR - unzip $SAFARI_DIR/dist-safari.zip -d $GITHUB_WORKSPACE/browser-build-artifacts - - - name: Load Safari extension for App Store - run: | - mkdir PlugIns - cp -r $GITHUB_WORKSPACE/browser-build-artifacts/Safari/masdev/build/Release/safari.appex PlugIns/safari.appex - - - name: Set up private auth key - run: | - mkdir ~/private_keys - cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} - EOF - - - name: Build dev application for App Store - env: - APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} - APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 - run: npm run pack:mac:masdev - - - name: Zip masdev asset - run: | - cd dist/mas-dev-universal - zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - - - name: Upload masdev artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip - path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip - if-no-files-found: error - - crowdin-push: name: Crowdin Push if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' diff --git a/.vscode/settings.json b/.vscode/settings.json index 295c290a37a..8f89bc03b8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "cSpell.words": ["Csprng", "decryptable", "Popout", "Reprompt", "takeuntil"], + "cSpell.words": ["Csprng", "Decapsulation", "decryptable", "Popout", "Reprompt", "takeuntil"], "search.exclude": { "**/locales/[^e]*/messages.json": true, "**/locales/*[^n]/messages.json": true, diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 1bd6a77b746..c6d51ff3f87 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -81,7 +81,7 @@ "message": "تلميح كلمة المرور الرئيسية (إختياري)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "درجة قوة كلمة المرور $SCORE$", "placeholders": { "score": { "content": "$1", @@ -186,7 +186,7 @@ "message": "نسخ الملاحظات" }, "copy": { - "message": "Copy", + "message": "نسخ", "description": "Copy to clipboard" }, "fill": { @@ -380,7 +380,7 @@ "message": "تحرير المجلّد" }, "editFolderWithName": { - "message": "Edit folder: $FOLDERNAME$", + "message": "تحرير المجلد: $FOLDERNAME$", "placeholders": { "foldername": { "content": "$1", @@ -395,7 +395,7 @@ "message": "أسم المجلد" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "قم بدمج مجلد بإضافة اسم المجلد الرئيسي متبوعًا بعلامة \"/\". مثال: اجتماعي/منتديات" }, "noFoldersAdded": { "message": "لا توجد مجلدات مضافة" @@ -462,16 +462,16 @@ "message": "توليد عبارة المرور" }, "passwordGenerated": { - "message": "Password generated" + "message": "مولد كلمة المرور" }, "passphraseGenerated": { - "message": "Passphrase generated" + "message": "تم إنشاء عبارة المرور" }, "usernameGenerated": { - "message": "Username generated" + "message": "تم إنشاء اسم المستخدم" }, "emailGenerated": { - "message": "Email generated" + "message": "تم إنشاء البريد الإلكتروني" }, "regeneratePassword": { "message": "إعادة توليد كلمة المرور" @@ -653,13 +653,13 @@ "message": "متصفح الويب الخاص بك لا يدعم خاصية النسخ السهل. يرجى استخدام النسخ اليدوي." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "قم بتأكيد هويتك" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "لم نتعرف على هذا الجهاز. أدخل الرمز المرسل إلى بريدك الإلكتروني للتحقق من هويتك." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "متابعة تسجيل الدخول" }, "yourVaultIsLocked": { "message": "خزانتك مقفلة. قم بتأكيد هويتك للمتابعة." @@ -869,19 +869,19 @@ "message": "تسجيل الدخول إلى Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "أدخل الرمز المرسل إلى بريدك الإلكتروني" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "أدخل الرمز من تطبيق المصادقة الخاص بك" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "اضغط على YubiKey الخاص بك للمصادقة" }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "تسجيل الدخول من خطوتين مطلوب لحسابك. اتبع الخطوات أدناه لإنهاء تسجيل الدخول." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "اتبع الخطوات أدناه لإنهاء تسجيل الدخول." }, "restartRegistration": { "message": "إعادة التسجيل" @@ -905,7 +905,7 @@ "message": "لا" }, "location": { - "message": "Location" + "message": "الموقع" }, "unexpectedError": { "message": "حدث خطأ غير متوقع." @@ -1010,7 +1010,7 @@ "message": "اطلب إضافة تسجيل الدخول" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "حفظ في خيارات المخزن" }, "addLoginNotificationDesc": { "message": "اطلب إضافة عنصر إذا لم يُعثر عليه في خزانتك." @@ -1019,7 +1019,7 @@ "message": "اطلب إضافة عنصر إذا لم يتم العثور على عنصر في المخزن الخاص بك. ينطبق على جميع حسابات تسجيل الدخول." }, "showCardsInVaultViewV2": { - "message": "Always show cards as Autofill suggestions on Vault view" + "message": "إظهار البطاقات دائمًا كاقتراحات التعبئة التلقائية في عرض المخزن" }, "showCardsCurrentTab": { "message": "أظهر البطاقات في صفحة التبويبات" @@ -1028,7 +1028,7 @@ "message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية." }, "showIdentitiesInVaultViewV2": { - "message": "Always show identities as Autofill suggestions on Vault view" + "message": "إظهار الهويات دائمًا كاقتراحات التعبئة التلقائية في عرض المخزن" }, "showIdentitiesCurrentTab": { "message": "إظهار الهويات على صفحة التبويب" @@ -1037,10 +1037,10 @@ "message": "قائمة عناصر الهوية في صفحة التبويب لسهولة الملء التلقائي." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "انقر فوق العناصر للملء التلقائي على عرض المخزن" }, "clickToAutofill": { - "message": "Click items in autofill suggestion to fill" + "message": "انقر فوق العناصر في اقتراح التعبئة التلقائية لملء" }, "clearClipboard": { "message": "مسح الحافظة", @@ -1057,7 +1057,7 @@ "message": "حفظ" }, "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", + "message": "حفظت في بيتواردن $USERNAME$.", "placeholders": { "username": { "content": "$1" @@ -1066,7 +1066,7 @@ "description": "Shown to user after login is saved." }, "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", + "message": "جددت في بيتواردن $USERNAME$.", "placeholders": { "username": { "content": "$1" @@ -1075,31 +1075,31 @@ "description": "Shown to user after login is updated." }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "حفظ كتسجيل دخول جديد", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "تحديث تسجيل الدخول", "description": "Button text for updating an existing login entry." }, "saveLoginPrompt": { - "message": "Save login?", + "message": "حفظ تسجيل الدخول؟", "description": "Prompt asking the user if they want to save their login details." }, "updateLoginPrompt": { - "message": "Update existing login?", + "message": "تحديث تسجيل الدخول الحالي؟", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "تم حفظ تسجيل الدخول", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "تم تحديث تسجيل الدخول", "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "عمل رائع! لقد اتخذت الخطوات لجعلك و $ORGANIZATION$ أكثر أمنا.", "placeholders": { "organization": { "content": "$1" @@ -1108,7 +1108,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "شكرا لك على جعل $ORGANIZATION$ أكثر أمنا. لديك $TASK_COUNT$ المزيد من كلمات المرور للتحديث.", "placeholders": { "organization": { "content": "$1" @@ -1120,15 +1120,15 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "تغيير كلمة المرور التالية", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { - "message": "Error saving", + "message": "خطأ في الحفظ", "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": "أوه لا! لم نتمكن من حفظ هذا. حاول إدخال التفاصيل يدويا.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -1213,26 +1213,26 @@ "message": "ستُستخدم كلمة المرور هذه لتصدير واستيراد هذا المِلَفّ" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "استخدم مفتاح تشفير حسابك، مشتقة من اسم المستخدم في حسابك وكلمة المرور الرئيسية، لتشفير التصدير وتقييد الاستيراد إلى حساب بيتواردن الحالي فقط." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "تعيين كلمة مرور للملف لتشفير التصدير واستيراده إلى أي حساب بيتواردن باستخدام كلمة المرور لفك التشفير." }, "exportTypeHeading": { - "message": "Export type" + "message": "نوع التصدير" }, "accountRestricted": { - "message": "Account restricted" + "message": "الحساب مقيد" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"كلمة مرور الملف\" و \"تأكيد كلمة مرور الملف\" غير متطابقين." }, "warning": { "message": "تحذير", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "تحذير", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1254,7 +1254,7 @@ "message": "مشترك" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "بيتواردن للأعمال التجارية يسمح لك بمشاركة عناصر المخزن الخاصة بك مع الآخرين باستخدام منظمة. تعرف على المزيد على موقع bitwarden.com على شبكة الإنترنت." }, "moveToOrganization": { "message": "الانتقال إلى مؤسسة" @@ -1312,7 +1312,7 @@ "message": "الملف" }, "fileToShare": { - "message": "File to share" + "message": "ملف للمشاركة" }, "selectFile": { "message": "حدد ملفًا" @@ -1348,7 +1348,7 @@ "message": "1 جيغابايت وحدة تخزين مشفرة لمرفقات الملفات." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "الوصول الطارئ." }, "premiumSignUpTwoStepOptions": { "message": "خيارات تسجيل الدخول بخطوتين المملوكة لجهات اخرى مثل YubiKey و Duo." @@ -1369,7 +1369,7 @@ "message": "شراء العضوية المميزة" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "يمكنك شراء العضوية المميزة من إعدادات حسابك على تطبيق بيتواردن على شبكة الإنترنت." }, "premiumCurrentMember": { "message": "أنت عضو مميز!" @@ -1378,7 +1378,7 @@ "message": "شكرا لك على دعم Bitwarden." }, "premiumFeatures": { - "message": "Upgrade to Premium and receive:" + "message": "الترقية إلى النسخة الممتازة و استلام:" }, "premiumPrice": { "message": "الكل فقط بـ $PRICE$ /سنة!", @@ -1390,7 +1390,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "كل شيء فقط $PRICE$ في السنة!", "placeholders": { "price": { "content": "$1", @@ -1417,10 +1417,10 @@ "message": "هذه المِيزة متاحة فقط للعضوية المميزة." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "مهلة المصادقة" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "انتهت مهلة جلسة المصادقة. الرجاء إعادة تشغيل عملية تسجيل الدخول." }, "verificationCodeEmailSent": { "message": "تم إرسال رسالة التحقق إلى $EMAIL$.", @@ -1432,29 +1432,29 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "لا تسأل مرة أخرى على هذا الجهاز لمدة 30 يوماً" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "اختر طريقة أخرى", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "استخدم رمز الاسترداد الخاص بك" }, "insertU2f": { "message": "أدخل مفتاح الأمان الخاص بك في منفذ USB كمبيوترك، إذا كان يحتوي على زر، إلمسه." }, "openInNewTab": { - "message": "Open in new tab" + "message": "فتح في علامة تبويب جديدة" }, "webAuthnAuthenticate": { "message": "مصادقة WebAuthn" }, "readSecurityKey": { - "message": "Read security key" + "message": "قراءة مفتاح الأمان" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "في انتظار التفاعل مع مفتاح الأمن..." }, "loginUnavailable": { "message": "تسجيل الدخول غير متاح" @@ -1469,7 +1469,7 @@ "message": "خيارات تسجيل الدخول بخطوتين" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "حدد طريقة تسجيل الدخول بخطوتين" }, "recoveryCodeDesc": { "message": "هل تفقد الوصول إلى جميع مزودي التحقق بعاملين؟ استخدم رمز الاسترداد الخاص بك لتعطيل جميع مزودي التحقق بعاملين من حسابك." @@ -1481,17 +1481,17 @@ "message": "تطبيق المصادقة" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "أدخل رمز تم إنشاؤه بواسطة تطبيق مصادقة مثل Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "مفتاح أمان OTP Yubico" }, "yubiKeyDesc": { "message": "استخدم YubiKey للوصول إلى حسابك. يعمل مع YubiKey 4 ،4 Nano ،4C، وأجهزة NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "أدخل الرمز الذي تم إنشاؤه بواسطة نظام حماية الثنائي.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1508,19 +1508,19 @@ "message": "البريد الإلكتروني" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "أدخل رمز مرسل إلى بريدك الإلكتروني." }, "selfHostedEnvironment": { "message": "البيئة المستضافة ذاتيا" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "حدد عنوان URL الأساسي لمبانيك التي استضافت تثبيت بتواردن على سبيل المثال: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "للتكوين المتقدم، يمكنك تحديد عنوان URL الأساسي لكل خدمة بشكل مستقل." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "يجب عليك إضافة رابط الخادم الأساسي أو على الأقل بيئة مخصصة." }, "customEnvironment": { "message": "بيئة مخصصة" @@ -1529,7 +1529,7 @@ "message": "رابط الخادم" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "رابط خادم الاستضافة الذاتية", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1555,22 +1555,22 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "اقتراحات التعبئة التلقائية" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "إظهار اقتراحات التعبئة التلقائية في حقول النموذج" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "عرض الهويات كاقتراحات" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "عرض البطاقات كاقتراحات" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "عرض الاقتراحات عند تحديد الأيقونة" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "ينطبق على جميع الحسابات المسجل الدخول بها." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "إيقاف تشغيل إعدادات مدير كلمات المرور الافتراضي في متصفحك لتجنب التضارب." @@ -1591,7 +1591,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "التعبئة التلقائية عند تحميل الصفحة" }, "enableAutoFillOnPageLoad": { "message": "ملء تلقائي عند تحميل الصفحة" @@ -1603,7 +1603,7 @@ "message": "مواقع المساومة أو غير الموثوق بها يمكن أن تستغل الملء التلقائي في تحميل الصفحة." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "معرفة المزيد عن المخاطر" }, "learnMoreAboutAutofill": { "message": "تعرف على المزيد حول الملء التلقائي" @@ -1633,13 +1633,13 @@ "message": "فتح المخزن في الشريط الجانبي" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "ملء تلقائي لآخر تسجيل دخول مستخدم للموقع الحالي" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "ملء تلقائي آخر بطاقة مستخدمة للموقع الحالي" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "ملء تلقائي آخر هوية مستخدمة للموقع الحالي" }, "commandGeneratePasswordDesc": { "message": "إنشاء واستنساخ كلمة مرور عشوائية جديدة إلى الحافظة" @@ -1663,7 +1663,7 @@ "message": "اسحب للفرز" }, "dragToReorder": { - "message": "Drag to reorder" + "message": "اسحب لإعادة الترتيب" }, "cfTypeText": { "message": "نص" @@ -1675,7 +1675,7 @@ "message": "قيمة منطقية" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "خانة" }, "cfTypeLinked": { "message": "مرتبط", @@ -1860,10 +1860,10 @@ "message": "الهوية" }, "typeSshKey": { - "message": "SSH key" + "message": "مفتاح SSH" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "جديد $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1872,7 +1872,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "تحرير $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1881,7 +1881,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "عرض $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1893,13 +1893,13 @@ "message": "سجل كلمة المرور" }, "generatorHistory": { - "message": "Generator history" + "message": "تاريخ المولدات" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "مسح سجل المولدات" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "إذا قمت بالمتابعة، سيتم حذف جميع الإدخالات بشكل دائم من سجل المولد. هل أنت متأكد من أنك تريد المتابعة؟" }, "back": { "message": "رجوع" @@ -1908,7 +1908,7 @@ "message": "المجموعات" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "مجموعات $COUNT$", "placeholders": { "count": { "content": "$1", @@ -1938,7 +1938,7 @@ "message": "ملاحظات آمنة" }, "sshKeys": { - "message": "SSH Keys" + "message": "مفاتيح SSH" }, "clear": { "message": "مسح", @@ -1964,7 +1964,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "النطاق الأساسي (مستحسن)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -2018,7 +2018,7 @@ "message": "لا توجد كلمات مرور للعرض." }, "clearHistory": { - "message": "Clear history" + "message": "مسح المحفوظات" }, "nothingToShow": { "message": "لا يوجد شيء لعرضه" @@ -2139,7 +2139,7 @@ "message": "مولد اسم المستخدم" }, "useThisEmail": { - "message": "Use this email" + "message": "استخدم هذا البريد الإلكتروني" }, "useThisPassword": { "message": "استخدم كلمة المرور هذه" @@ -2159,22 +2159,22 @@ "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": "تخصيص المخزن" }, "vaultTimeoutAction": { "message": "إجراء مهلة المخزن" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "إجراء المهلة" }, "newCustomizationOptionsCalloutTitle": { - "message": "New customization options" + "message": "خيارات التخصيص الجديدة" }, "newCustomizationOptionsCalloutContent": { - "message": "Customize your vault experience with quick copy actions, compact mode, and more!" + "message": "تخصيص تجربة المخزن الخاص بك مع إجراءات النسخ السريعة، والوضع المدمج، والمزيد!" }, "newCustomizationOptionsCalloutLink": { - "message": "View all Appearance settings" + "message": "عرض جميع إعدادات المظهر" }, "lock": { "message": "قفل", @@ -2323,7 +2323,7 @@ "message": "سياسة الخصوصية" }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "كلمة المرور الجديدة لا يمكن أن تكون نفس كلمة المرور الحالية." }, "hintEqualsPassword": { "message": "لا يمكن أن يكون تلميح كلمة المرور نفس كلمة المرور الخاصة بك." @@ -2332,10 +2332,10 @@ "message": "موافق" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "خطأ في تحديث رمز الوصول" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "لم يتم العثور على رمز تحديث أو مفاتيح API. الرجاء محاولة تسجيل الخروج وتسجيل الدخول مرة أخرى." }, "desktopSyncVerificationTitle": { "message": "التحقق من مزامنة سطح المكتب" @@ -2377,7 +2377,7 @@ "message": "عدم تطابق المفتاح الحيوي" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "فشل فتح القفل البيومتري. فشل المفتاح السري الحيوي في فتح خزانة. الرجاء محاولة إعداد القياسات الحيوية مرة أخرى." }, "biometricsNotEnabledTitle": { "message": "لم يتم إعداد القياسات الحيوية" @@ -2392,16 +2392,16 @@ "message": "القياسات الحيوية للمتصفح غير مدعومة على هذا الجهاز." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "المستخدم مقفل أو مسجل الخروج" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "الرجاء فتح هذا المستخدم في تطبيق سطح المكتب وحاول مرة أخرى." }, "biometricsNotAvailableTitle": { - "message": "Biometric unlock unavailable" + "message": "فتح القفل الحيوي غير متوفر" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "الفتح الحيوي غير متوفر حاليا. الرجاء المحاولة مرة أخرى لاحقاً." }, "biometricsFailedTitle": { "message": "فشل القياسات الحيوية" @@ -2425,20 +2425,20 @@ "message": "بسبب سياسة المؤسسة، يمنع عليك حفظ العناصر في خزانتك الشخصية. غيّر خيار الملكية إلى مؤسسة واختر من المجموعات المتاحة." }, "personalOwnershipPolicyInEffect": { - "message": "An organization policy is affecting your ownership options." + "message": "تؤثر سياسة مؤسسة على خيارات الملكية الخاصة بك." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "لقد حالت سياسة المؤسسة دون استيراد العناصر إلى خزانتك الشخصية." }, "domainsTitle": { "message": "النطاقات", "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "النطاقات المحظورة" }, "learnMoreAboutBlockedDomains": { - "message": "Learn more about blocked domains" + "message": "معرفة المزيد حول النطاقات المحظورة" }, "excludedDomains": { "message": "النطاقات المستبعدة" @@ -2450,19 +2450,19 @@ "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطافات لجميع الحسابات مسجلة الدخول. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "لن يتم توفير الملء التلقائي والمميزات الأخرى ذات الصلة لهذه المواقع. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "التعبئة التلقائية محظورة لهذا الموقع." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "تغيير هذا في الإعدادات" }, "change": { - "message": "Change" + "message": "تغيير" }, "changeButtonTitle": { - "message": "Change password - $ITEMNAME$", + "message": "تغيير كلمة المرور - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -2715,27 +2715,27 @@ "message": "كلمة المرور الجديدة" }, "sendDisabled": { - "message": "Send removed", + "message": "تم إزالة الإرسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "Due to an enterprise policy, you are only able to delete an existing Send.", + "message": "بسبب سياسة المؤسسة، يمكنك فقط حذف إرسال موجود بالفعل.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Send created", + "message": "تم إنشاء إرسال جديد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "تم إنشاء الإرسال بنجاح!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "سيكون الإرسال متاحا لأي شخص لديه الرابط لمدة ساعة واحدة قادمة.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "سيكون الإرسال متاحا لأي شخص لديه الرابط لساعات $HOURS$ القادمة.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2745,11 +2745,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "سيكون الإرسال متاحا لأي شخص لديه الرابط لليوم التالي.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "سيكون الإرسال متاحا لأي شخص لديه الرابط خلال الأيام $DAYS$ القادمة.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2759,32 +2759,32 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "إرسال رابط منسوخ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Send saved", + "message": "إرسال محفوظ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "تمديد مستخرج؟", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "لإنشاء إرسال ملف، تحتاج إلى تثبيت الملحق إلى نافذة جديدة.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "In order to choose a file, open the extension in the sidebar (if possible) or pop out to a new window by clicking this banner." + "message": "من أجل اختيار ملف، قم بفتح الملحق في الشريط الجانبي (إن أمكن) أو الإنتقال إلى نافذة جديدة بالنقر على هذا الشعار." }, "sendFirefoxFileWarning": { - "message": "In order to choose a file using Firefox, open the extension in the sidebar or pop out to a new window by clicking this banner." + "message": "من أجل اختيار ملف باستخدام فايرفوكس، افتح الملحق في الشريط الجانبي أو اخرج إلى نافذة جديدة من خلال النقر على هذا الشعار." }, "sendSafariFileWarning": { - "message": "In order to choose a file using Safari, pop out to a new window by clicking this banner." + "message": "من أجل اختيار ملف باستخدام سافاري، انتقل إلى نافذة جديدة بالنقر على هذا الشعار." }, "popOut": { - "message": "Pop out" + "message": "انبثق" }, "sendFileCalloutHeader": { "message": "قبل أن تبدأ" @@ -2796,19 +2796,19 @@ "message": "صلاحية تاريخ الحذف المقدّم غير صحيح." }, "expirationDateAndTimeRequired": { - "message": "An expiration date and time are required." + "message": "مطلوب تاريخ ووقت انتهاء الصلاحية." }, "deletionDateAndTimeRequired": { - "message": "A deletion date and time are required." + "message": "مطلوب تاريخ ووقت الحذف." }, "dateParsingError": { - "message": "There was an error saving your deletion and expiration dates." + "message": "حدث خطأ أثناء حفظ تواريخ الحذف وانتهاء الصلاحية." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "إخفاء عنوان البريد الإلكتروني الخاص بك من المشاهدين." }, "passwordPrompt": { - "message": "Master password re-prompt" + "message": "إعادة توجيه كلمة المرور الرئيسية" }, "passwordConfirmation": { "message": "تأكيد كلمة المرور الرئيسية" @@ -2820,13 +2820,13 @@ "message": "تأكيد البريد الإلكتروني مطلوب" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "تم التحقق من البريد الإلكتروني" }, "emailVerificationRequiredDesc": { "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزانة الويب." }, "updatedMasterPassword": { - "message": "Updated master password" + "message": "تحديث كلمة المرور الرئيسية" }, "updateMasterPassword": { "message": "تحديث كلمة المرور الرئيسية" @@ -2835,10 +2835,10 @@ "message": "تم تغيير كلمة المرور الرئيسية الخاصة بك مؤخرًا من قبل مسؤول في مؤسستك. من أجل الوصول إلى الخزانة، يجب عليك تحديثها الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. قد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "كلمة المرور الرئيسية الخاصة بك لا تفي بواحدة أو أكثر من سياسات مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديث كلمة المرور الرئيسية الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. وقد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "لقد قامت مؤسستك بتعطيل تشفير الجهاز الموثوق به. الرجاء تعيين كلمة مرور رئيسية للوصول إلى خزانك." }, "resetPasswordPolicyAutoEnroll": { "message": "التسجيل التلقائي" @@ -2847,22 +2847,22 @@ "message": "هذه المؤسسة لديها سياسة تقوم تلقائياً بتسجيلك في إعادة تعيين كلمة المرور. هذا التسجيل سيسمح لمسؤولي المؤسسة بتغيير كلمة المرور الرئيسية الخاصة بك." }, "selectFolder": { - "message": "Select folder..." + "message": "حدد المجلد..." }, "noFoldersFound": { - "message": "No folders found", + "message": "لم يتم العثور على أي مجلدات", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "تم تحديث أذونات مؤسستك، مما يتطلب عليك تعيين كلمة مرور رئيسية.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "تتطلب مؤسستك تعيين كلمة مرور رئيسية.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "من $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2881,7 +2881,7 @@ "message": "دقائق" }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Enterprise policy requirements have been applied to your timeout options" + "message": "تم تطبيق متطلبات سياسة المؤسسة على خيارات المهلة الخاصة بك" }, "vaultTimeoutPolicyInEffect": { "message": "سياسات مؤسستك تؤثر على مهلة الخزانة الخاص بك. الحد الأقصى المسموح به لمهلة الخزانة هو $HOURS$ ساعة/ساعات و $MINUTES$ دقيقة/دقائق.", @@ -2897,7 +2897,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "$HOURS$ ساعة(ساعات) و $MINUTES$ دقيقة(دقائق) كحد أقصى.", "placeholders": { "hours": { "content": "$1", @@ -2910,7 +2910,7 @@ } }, "vaultTimeoutPolicyMaximumError": { - "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "message": "تجاوز المهلة القيد الذي تضعه مؤسستك: $HOURS$ ساعة(ساعات) و $MINUTES$ دقيقة(دقائق) كحد أقصى", "placeholders": { "hours": { "content": "$1", @@ -2923,7 +2923,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "سياسات مؤسستك تؤثر على مهلة خزانتك. الحد الأقصى المسموح به لمهلة الخزانة هو $HOURS$ ساعة(ساعات) و $MINUTES$ دقيقة(دقائق). يتم تعيين إجراء مهلة المخزن الخاص بك إلى $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -2940,7 +2940,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "لقد حددت سياسات مؤسستك إجراء مهلة المخزن الخاص بك إلى $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2964,7 +2964,7 @@ "message": "لم يتم العثور على معرف فريد." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", + "message": "يستخدم $ORGANIZATION$ SSO مع خادم مفتاح الاستضافة الذاتية. لم تعد هناك حاجة إلى كلمة مرور رئيسية لتسجيل الدخول لأعضاء هذه المنظمة.", "placeholders": { "organization": { "content": "$1", @@ -2988,7 +2988,7 @@ "message": "لقد غادرت المؤسسة." }, "toggleCharacterCount": { - "message": "Toggle character count" + "message": "تبديل عدد الأحرف" }, "sessionTimeout": { "message": "انتهت مدة جلستك. يرجى العودة ومحاولة تسجيل الدخول مرة أخرى." @@ -2997,7 +2997,7 @@ "message": "جاري تصدير الخزانة الشخصية" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "سيتم تصدير عناصر المخزن الفردية المرتبطة بـ $EMAIL$ فقط. لن يتم إدراج عناصر مخزن المنظمة. سيتم تصدير معلومات المنتج فقط ولن تشمل المرفقات المرتبطة بها.", "placeholders": { "email": { "content": "$1", @@ -3006,7 +3006,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "فقط عناصر المخزن الفردية بما في ذلك المرفقات المرتبطة بـ $EMAIL$ سيتم تصديرها. لن يتم تضمين عناصر مخزن المنظمة", "placeholders": { "email": { "content": "$1", @@ -3015,10 +3015,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "خزانة منظمة التصدير" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "فقط مستودع المنظمة المرتبط بـ $ORGANIZATION$ سيتم تصديره. لن يتم إدراج العناصر في المستودعات الفردية أو المنظمات الأخرى.", "placeholders": { "organization": { "content": "$1", @@ -3030,27 +3030,27 @@ "message": "خطأ" }, "decryptionError": { - "message": "Decryption error" + "message": "خطأ فك التشفير" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "تعذر على بتواردن فك تشفير العنصر (العناصر) المدرجة أدناه." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "تم الاتصال بالعميل بنجاح", "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": "لتجنب فقدان بيانات إضافية.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "إنشاء اسم المستخدم" }, "generateEmail": { - "message": "Generate email" + "message": "إنشاء بريد إلكتروني" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "يجب أن تكون القيمة بين $MIN$ و $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -3064,7 +3064,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " استخدم أحرف $RECOMMENDED$ أو أكثر لإنشاء كلمة مرور قوية.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3074,7 +3074,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " استخدم كلمات $RECOMMENDED$ أو أكثر لإنشاء عبارة مرور قوية.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3084,7 +3084,7 @@ } }, "plusAddressedEmail": { - "message": "Plus addressed email", + "message": "بريد إلكتروني إضافي", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 41e6d9b5ab9..ee32999daca 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -1099,7 +1099,7 @@ "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "Əhsən sizə! Özünüzü və $ORGANIZATION$ təşkilatını daha güvənli etmək üçün addımlar atdınız.", "placeholders": { "organization": { "content": "$1" @@ -1108,7 +1108,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "$ORGANIZATION$ təşkilatını daha güvənli hala gətirdiyiniz üçün təşəkkürlər. Daha $TASK_COUNT$ parolunuz güncəllənməlidir.", "placeholders": { "organization": { "content": "$1" @@ -1120,7 +1120,7 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "Növbəti parolu dəyişdir", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { @@ -4928,8 +4928,8 @@ "message": "Parol yenidən yaradıldı", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Giriş Bitwarden-də saxlanılsın?", + "saveToBitwarden": { + "message": "\"Bitwarden\"də saxla", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index f828400c575..9310d8830f4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 457b0693a17..6908ee7564e 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -4928,8 +4928,8 @@ "message": "Паролата е прегенерирана", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Запазване на данните за вписване в Битуорден?", + "saveToBitwarden": { + "message": "Запазване в Битуорден", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 275bb18e029..f4bf9cb9e27 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 687610c777e..6b6f86cda37 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index d9a24c5e473..fbd752305a6 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -4928,8 +4928,8 @@ "message": "Contrasenya regenerada", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 2ed34c8c71a..f413fe30298 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -4928,8 +4928,8 @@ "message": "Heslo bylo znovu vygenerováno", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Uložit přihlášení do Bitwardenu?", + "saveToBitwarden": { + "message": "Uložit do Bitwardenu", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index eb07358540d..592e0e38e73 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 914c74411e4..43cae7d9c50 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -4928,8 +4928,8 @@ "message": "Adgangskode genereret igen", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Gem login til Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 0edf5edfa3d..dad00bce98d 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1099,7 +1099,7 @@ "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "Gut gemacht! Du hast die Schritte unternommen, um dich und $ORGANIZATION$ sicherer zu machen.", "placeholders": { "organization": { "content": "$1" @@ -1108,7 +1108,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "Vielen Dank, dass du $ORGANIZATION$ sicherer gemacht hast. Du hast $TASK_COUNT$ weitere Passwörter zum Aktualisieren.", "placeholders": { "organization": { "content": "$1" @@ -1120,7 +1120,7 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "Nächstes Passwort ändern", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { @@ -4928,8 +4928,8 @@ "message": "Passwort neu generiert", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Zugangsdaten in Bitwarden speichern?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 1c15f70d40c..7cdb3f14ec3 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -4928,8 +4928,8 @@ "message": "Ο κωδικός επαναδημιουργήθηκε", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Αποθήκευση σύνδεσης στο Bitwarden;", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index b0f3fb5d19e..d1d05a4e852 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -192,7 +192,7 @@ "message": "Copy", "description": "Copy to clipboard" }, - "fill":{ + "fill": { "message": "Fill", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, @@ -886,6 +886,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "restartRegistration": { "message": "Restart registration" }, @@ -1059,23 +1062,45 @@ "notificationAddSave": { "message": "Save" }, - "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", - "placeholders": { - "username": { - "content": "$1" - } - }, - "description": "Shown to user after login is saved." + "notificationViewAria": { + "message": "View $ITEMNAME$, opens in new window", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Aria label for the view button in notification bar confirmation message" }, - "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", - "placeholders": { - "username": { - "content": "$1" - } - }, - "description": "Shown to user after login is updated." + "newNotification": { + "message": "New notification" + }, + "labelWithNotification": { + "message": "$LABEL$: New notification", + "description": "Label for the notification with a new login suggestion.", + "placeholders": { + "label": { + "content": "$1", + "example": "Login" + } + } + }, + "loginSaveConfirmation": { + "message": "$ITEMNAME$ saved to Bitwarden.", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Shown to user after item is saved." + }, + "loginUpdatedConfirmation": { + "message": "$ITEMNAME$ updated in Bitwarden.", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Shown to user after item is updated." }, "saveAsNewLoginAction": { "message": "Save as new login", diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 520af1ca8ff..0be594e67ba 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index f75664f10db..94ec0753410 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 1400cb78845..3ddea49820d 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -380,7 +380,7 @@ "message": "Editar carpeta" }, "editFolderWithName": { - "message": "Edit folder: $FOLDERNAME$", + "message": "Editar carpeta: $FOLDERNAME$", "placeholders": { "foldername": { "content": "$1", @@ -872,7 +872,7 @@ "message": "Enter the code sent to your email" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Introduce el código de tu aplicación de autenticación" }, "pressYourYubiKeyToAuthenticate": { "message": "Press your YubiKey to authenticate" @@ -1445,7 +1445,7 @@ "message": "Inserta tu llave de seguridad en el puerto USB de tu equipo. Si tiene un botón, púlsalo." }, "openInNewTab": { - "message": "Open in new tab" + "message": "Abrir en nueva pestaña" }, "webAuthnAuthenticate": { "message": "Autenticar WebAuthn" @@ -1663,7 +1663,7 @@ "message": "Arrastrar para ordenar" }, "dragToReorder": { - "message": "Drag to reorder" + "message": "Arrastra para reordenar" }, "cfTypeText": { "message": "Texto" @@ -3614,10 +3614,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 campo necesita tu atención." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "Los campos $COUNT$ necesitan tu atención.", "placeholders": { "count": { "content": "$1", @@ -4081,7 +4081,7 @@ "message": "Cuenta activa" }, "bitwardenAccount": { - "message": "Bitwarden account" + "message": "Cuenta de Bitwarden" }, "availableAccounts": { "message": "Cuentas disponibles" @@ -4179,7 +4179,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "¡Contraseña guardada!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -4515,10 +4515,10 @@ "message": "Autofill options" }, "websiteUri": { - "message": "Website (URI)" + "message": "Página web (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Página web (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4589,7 +4589,7 @@ "message": "Activar animaciones" }, "showAnimations": { - "message": "Show animations" + "message": "Mostrar animaciones" }, "addAccount": { "message": "Añadir cuenta" @@ -4661,10 +4661,10 @@ "message": "Enter the the field's html id, name, aria-label, or placeholder." }, "editField": { - "message": "Edit field" + "message": "Editar campo" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "Editar $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4828,16 +4828,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clave privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clave pública" }, "sshFingerprint": { "message": "Fingerprint" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipo de clave" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4928,8 +4928,8 @@ "message": "Contraseña generada", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "¿Guardar inicio de sesión en Bitwarden?", + "saveToBitwarden": { + "message": "Guardar en Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { @@ -5080,7 +5080,7 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Aviso importante" }, "setupTwoStepLogin": { "message": "Set up two-step login" @@ -5092,10 +5092,10 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Recuérdame más tarde" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "¿Tienes acceso fiable a tu correo electrónico, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -5104,7 +5104,7 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "No, no lo tengo" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { "message": "Sí, puedo acceder a mi correo electrónico de forma fiable" @@ -5125,16 +5125,16 @@ "message": "Extraancho" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "La contraseña introducida es incorrecta." }, "importSshKey": { - "message": "Import" + "message": "Importar" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Confirmar contraseña" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Introduce la contraseña para la clave SSH." }, "enterSshKeyPassword": { "message": "Enter password" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index f9f7cb950fe..46731c85cfa 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ec1f5189946..687e6a52473 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 0e5e8943b7d..8e94676b17e 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 4048925ebaa..5e9a14d6aaf 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1099,7 +1099,7 @@ "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "Hienoa työtä! Otit askeleita, joilla teet itsestäsi ja organisaatiosta $ORGANIZATION$ turvallisemman.", "placeholders": { "organization": { "content": "$1" @@ -1108,7 +1108,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "Kiitos, että teet organisaatiosta $ORGANIZATION$ turvallisemman. Sinulla on vielä $TASK_COUNT$ salasanaa päivitettävänä.", "placeholders": { "organization": { "content": "$1" @@ -1120,7 +1120,7 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "Vaihda seuraava salasana", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { @@ -2323,7 +2323,7 @@ "message": "Tietosuojakäytäntö" }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "Uusi salasanasi ei voi olla sama kuin nykyinen salasanasi." }, "hintEqualsPassword": { "message": "Salasanavihjeesi ei voi olla sama kuin salasanasi." @@ -2533,14 +2533,14 @@ "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { - "message": "Illustration of a list of logins that are at-risk." + "message": "Kuvitus vaarantuneiden kirjautumistietojen luettelosta." }, "generatePasswordSlideDesc": { "message": "Luo vahva ja ainutlaatuinen salasana nopeasti Bitwardenin automaattitäytön valikosta vaarantuneella sivustolla.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password." + "message": "Kuvitus Bitwardenin automaattitäytön valikosta, luodulla salasanalla." }, "updateInBitwarden": { "message": "Päivitä Bitwardenissa" @@ -2550,7 +2550,7 @@ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." + "message": "Kuvitus ilmoituksesta, jossa Bitwarden tarjoaa kirjautumistiedon päivitystä." }, "turnOnAutofill": { "message": "Ota automaattitäyttö käyttöön" @@ -3006,7 +3006,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "Vain henkilökohtaiset tunnukseen $EMAIL$ liittyvät holvin kohteet liitetiedostoineen viedään. Organisaation holvikohteita ei sisällytetä", "placeholders": { "email": { "content": "$1", @@ -4272,7 +4272,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "Tarkastele kohdetta - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4296,7 +4296,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "Automaattitäytä - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4928,8 +4928,8 @@ "message": "Salasana luotiin uudelleen", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Tallennetaanko kirjautumistieto Bitwardeniin?", + "saveToBitwarden": { + "message": "Tallenna Bitwardeniin", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { @@ -5170,30 +5170,30 @@ "message": "Vaihda vaarantunut salasana" }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Tervetuloa Bitwardeniin" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Tietoturva etusijalla" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Tallenna kirjautumistiedot, kortit ja henkilöllisyydet suojattuun holviisi. Bitwarden suojaa tärkeät tietosi nollatietoisella päästä päähän -salauksella." }, "quickLogin": { - "message": "Quick and easy login" + "message": "Nopea ja helppo kirjautuminen" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Määritä biometrinen avaus ja automaattitäyttö kirjautuaksesi tileillesi kirjoittamatta yhtäkään kirjainta." }, "secureUser": { - "message": "Level up your logins" + "message": "Nosta kirjautumisesi uudelle tasolle" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Käytä generaattoria luodaksesi ja tallentaaksesi vahvoja, ainutlaatuisia salasanoja kaikille tileillesi." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Sinun tietosi, missä ja milloin tarvitset niitä" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Tallenna rajattomasti salasanoja, rajattomalla määrällä laitteita, Bitwardenin mobiili-, selain- ja työpöytäsovelluksilla." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 9c3865bc0da..14820d62f45 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 1f05f699ea9..a41c958c6c6 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -4928,8 +4928,8 @@ "message": "Mot de passe régénéré", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Enregistrer l'identifiant sur Bitwarden ?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b2b843d7830..dc855d6e230 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -4928,8 +4928,8 @@ "message": "Contrasinal xerado de novo", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Gardar credenciais en Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index fd38df53906..1c5ece85f96 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -4928,8 +4928,8 @@ "message": "סיסמה נוצרה מחדש", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "לשמור כניסה ב־Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 813b69b9079..368f1e7a828 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 896593332a4..72f14216020 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -4928,8 +4928,8 @@ "message": "Lozinka re-generirana", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Spremi prijavu u Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index f58e542c77f..d4b86eca2ae 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -4928,8 +4928,8 @@ "message": "A jelszó generálásra került.", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Bejelentkezés mentése a Bitwardenbe?", + "saveToBitwarden": { + "message": "Mentés a Bitwardenbe", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 101fcf43795..3efc404a9a0 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -4928,8 +4928,8 @@ "message": "Kata sandi dibuat ulang", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Simpan log masuk ke Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 5f9a6ca7dca..fa8ed288cfc 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -4928,8 +4928,8 @@ "message": "Password rigenerata", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Salvare il login su Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 34b15699437..1f6aa5e0045 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -4928,8 +4928,8 @@ "message": "パスワードを再生成しました", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Bitwarden にログイン情報を保存しますか?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index f261f69904d..613b876c67b 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 7bec167e1c6..61d21cc2987 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 7d9583dc652..229842d993b 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 7d65686df5d..5e8f2dec4c5 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -4928,8 +4928,8 @@ "message": "비밀번호가 재생성되었습니다.", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Bitwarden에 로그인을 저장하시겠습니까?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 2245fdb0029..88dbac2b1ab 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 517dacfb72b..e1de9bd01b5 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -4928,8 +4928,8 @@ "message": "Parole pārizveidota", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Saglabāt pieteikšanās vienumu Bitwarden?", + "saveToBitwarden": { + "message": "Saglabāt Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 49a2695ce6f..478086b5f7d 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 6b2f7b8bc32..5bfc85f9828 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 7bec167e1c6..61d21cc2987 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 7fa41eed68b..06ff737edf7 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -4928,8 +4928,8 @@ "message": "Passord ble generert på nytt", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Vil du lagre påloggingen i Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 7bec167e1c6..61d21cc2987 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index dd101e62e75..e39405a8f73 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -4928,8 +4928,8 @@ "message": "Wachtwoord opnieuw gegenereerd", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Login opslaan in Bitwarden?", + "saveToBitwarden": { + "message": "Opslaan in Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 7bec167e1c6..61d21cc2987 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 7bec167e1c6..61d21cc2987 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 3d25e6f123c..e0b66fc43f8 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -4928,8 +4928,8 @@ "message": "Hasło zostało ponownie wygenerowane", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Zapisać dane logowania w Bitwarden?", + "saveToBitwarden": { + "message": "Zapisz w Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 9d1744f5473..a6d224587f1 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -4928,8 +4928,8 @@ "message": "Senha regenerada", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Salvar login no Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index daab7fb05d9..4c95f0d6533 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -4928,8 +4928,8 @@ "message": "Palavra-passe gerada novamente", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Guardar credencial no Bitwarden?", + "saveToBitwarden": { + "message": "Guardar no Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index bcdf10f37f1..6f870a7e775 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index b0f03d34f69..00657d94978 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -4928,8 +4928,8 @@ "message": "Пароль сгенерирован", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Сохранить логин в Bitwarden?", + "saveToBitwarden": { + "message": "Сохранить в Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 4efbb2dceeb..4a99c185afa 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index b427e902334..d00478ee69c 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -4928,8 +4928,8 @@ "message": "Vygenerované nové heslo", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Uložiť prihlasovacie údaje do Bitwardenu?", + "saveToBitwarden": { + "message": "Uložiť do Bitwardenu", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 61a70650d16..1703fba36c5 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index a36006894ba..8c36a163039 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -4928,8 +4928,8 @@ "message": "Лозинка поново генерисана", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Сачувати пријаву на Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 54a34e15013..0796b67cf0f 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Spara inloggning på Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 7bec167e1c6..61d21cc2987 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index b502e29cfdd..e8e61d6bcb7 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 8a00287dfcc..8038d415b64 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -4928,8 +4928,8 @@ "message": "Parola yeniden üretildi", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Hesap Bitwarden'a kaydedilsin mi?", + "saveToBitwarden": { + "message": "Bitwarden’a kaydet", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index bdd96ba9ea4..9b8974cdf6b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -4928,8 +4928,8 @@ "message": "Пароль згенеровано повторно", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Зберегти запис у Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index c6c6ed7ee6d..3a226681fb1 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 662284568cd..633f65c6c72 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -4928,8 +4928,8 @@ "message": "密码已重新生成", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "将登录保存到 Bitwarden 吗?", + "saveToBitwarden": { + "message": "保存到 Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 7d56ecd8e63..91797d314b2 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -4928,8 +4928,8 @@ "message": "密碼已重新產生", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "在 Bitwarden 中儲存登入資訊?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { 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 b8252aa6e13..ebf79af644c 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -23,7 +23,7 @@ = of(true); form = this.formBuilder.group({ @@ -147,11 +145,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { - // Firefox popup closes when unfocused by biometrics, blocking all unlock methods - if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) { - this.showAutoPrompt = false; - } - const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); this.showMasterPasswordOnClientRestartOption = hasMasterPassword; const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe( diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 6b3c91a109c..c93fd9a3acf 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -101,6 +101,10 @@ type NotificationBackgroundExtensionMessageHandlers = { bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + bgOpenViewVaultItemPopout: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index ffc416ab62a..bb993fcf94b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -823,6 +823,7 @@ describe("NotificationBackground", () => { notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ id: "testId", + name: "testItemName", login: { username: "testUser" }, }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -844,8 +845,9 @@ describe("NotificationBackground", () => { sender.tab, "saveCipherAttemptCompleted", { - username: cipherView.login.username, + itemName: "testItemName", cipherId: cipherView.id, + task: undefined, }, ); }); @@ -899,7 +901,7 @@ describe("NotificationBackground", () => { const cipherView = mock({ id: mockCipherId, organizationId: mockOrgId, - login: { username: "testUser" }, + name: "Test Item", }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -921,11 +923,11 @@ describe("NotificationBackground", () => { "saveCipherAttemptCompleted", { cipherId: "testId", + itemName: "Test Item", task: { orgName: "Org Name, LLC", remainingTasksCount: 1, }, - username: "testUser", }, ); }); @@ -1074,6 +1076,7 @@ describe("NotificationBackground", () => { notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ id: "testId", + name: "testName", login: { username: "test", password: "password" }, }); folderExistsSpy.mockResolvedValueOnce(false); @@ -1097,8 +1100,8 @@ describe("NotificationBackground", () => { sender.tab, "saveCipherAttemptCompleted", { - username: cipherView.login.username, cipherId: cipherView.id, + itemName: cipherView.name, }, ); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 6589252d94b..dabb75b97b6 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, switchMap, map } from "rxjs"; +import { firstValueFrom, switchMap, map, of } 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"; @@ -41,7 +41,10 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; +import { + openAddEditVaultItemPopout, + openViewVaultItemPopout, +} from "../../vault/popup/utils/vault-popout-window"; import { OrganizationCategory, OrganizationCategories, @@ -67,6 +70,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr export default class NotificationBackground { private openUnlockPopout = openUnlockPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private openViewVaultItemPopout = openViewVaultItemPopout; private notificationQueue: NotificationQueueMessageItem[] = []; private allowedRetryCommands: Set = new Set([ ExtensionCommand.AutofillLogin, @@ -91,6 +95,7 @@ export default class NotificationBackground { bgGetOrgData: () => this.getOrgData(), bgNeverSave: ({ sender }) => this.saveNever(sender.tab), bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), + bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab), bgRemoveTabFromNotificationQueue: ({ sender }) => this.removeTabFromNotificationQueue(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), @@ -638,8 +643,8 @@ export default class NotificationBackground { try { await this.cipherService.createWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: queueMessage?.username && String(queueMessage.username), - cipherId: cipher?.id && String(cipher.id), + itemName: newCipher?.name && String(newCipher?.name), + cipherId: cipher?.id && String(cipher?.id), }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); } catch (error) { @@ -701,7 +706,7 @@ export default class NotificationBackground { await this.cipherService.updateWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: cipherView?.login?.username && String(cipherView.login.username), + itemName: cipherView?.name && String(cipherView?.name), cipherId: cipherView?.id && String(cipherView.id), task: taskData, }); @@ -754,6 +759,21 @@ export default class NotificationBackground { await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId }); } + private async viewItem( + message: NotificationBackgroundExtensionMessage, + senderTab: chrome.tabs.Tab, + ) { + await Promise.all([ + this.openViewVaultItemPopout(senderTab, { + cipherId: message.cipherId, + action: null, + }), + BrowserApi.tabSendMessageData(senderTab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }), + ]); + } + private async folderExists(folderId: string, userId: UserId) { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { return false; @@ -780,7 +800,7 @@ export default class NotificationBackground { this.taskService.tasksEnabled$(userId).pipe( switchMap((tasksEnabled) => { if (!tasksEnabled) { - return []; + return of([]); } return this.taskService diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index aaa4b11d8a2..85698c87c67 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -8,24 +8,24 @@ export function CipherAction({ handleAction = () => { /* no-op */ }, + i18n, notificationType, theme, }: { handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; theme: Theme; }) { return notificationType === NotificationTypes.Change ? BadgeButton({ buttonAction: handleAction, - // @TODO localize - buttonText: "Update", + buttonText: i18n.notificationUpdate, theme, }) : EditButton({ buttonAction: handleAction, - // @TODO localize - buttonText: "Edit", + buttonText: i18n.notificationEdit, theme, }); } diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts index 96b44d2c0cc..8ab29860f3b 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -19,11 +19,13 @@ const cipherIconWidth = "24px"; export function CipherItem({ cipher, handleAction, + i18n, notificationType, theme = ThemeTypes.Light, }: { cipher: NotificationCipherData; handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType?: NotificationType; theme: Theme; }) { @@ -34,7 +36,7 @@ export function CipherItem({ if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { cipherActionButton = html`
- ${CipherAction({ handleAction, notificationType, theme })} + ${CipherAction({ handleAction, i18n, notificationType, theme })}
`; } diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts index e597cddabe6..dd1ff816f06 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts @@ -7,6 +7,7 @@ import { CipherAction } from "../../cipher/cipher-action"; type Args = { handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; theme: Theme; }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts index 32b4170d1da..13e2322a9f2 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -10,6 +10,7 @@ import { NotificationBody } from "../../notification/body"; type Args = { ciphers: NotificationCipherData[]; + i18n: { [key: string]: string }; notificationType: NotificationType; theme: Theme; handleEditOrUpdateAction: (e: Event) => void; diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts index 66b580bde43..cc0fa359303 100644 --- a/apps/browser/src/autofill/content/components/notification/body.ts +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -17,12 +17,14 @@ const { css } = createEmotion({ export function NotificationBody({ ciphers = [], + i18n, notificationType, theme = ThemeTypes.Light, handleEditOrUpdateAction, }: { ciphers?: NotificationCipherData[]; customClasses?: string[]; + i18n: { [key: string]: string }; notificationType?: NotificationType; theme: Theme; handleEditOrUpdateAction: (e: Event) => void; @@ -37,6 +39,7 @@ export function NotificationBody({ theme, children: CipherItem({ cipher, + i18n, notificationType, theme, handleAction: handleEditOrUpdateAction, diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 8661f5957e1..3834da4269d 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -22,17 +22,19 @@ function getVaultIconByProductTier(productTierType?: ProductTierType): Option["i } export type NotificationButtonRowProps = { - theme: Theme; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; primaryButton: { text: string; handlePrimaryButtonClick: (args: any) => void; }; - folders?: FolderView[]; - organizations?: OrgView[]; + theme: Theme; }; export function NotificationButtonRow({ folders, + i18n, organizations, primaryButton, theme, @@ -40,7 +42,7 @@ export function NotificationButtonRow({ const currentUserVaultOption: Option = { icon: User, default: true, - text: "My vault", // @TODO localize + text: i18n.myVault, value: "0", }; const organizationOptions: Option[] = organizations?.length @@ -84,7 +86,7 @@ export function NotificationButtonRow({ ? [ { id: "organization", - label: "Vault", // @TODO localize + label: i18n.vault, options: organizationOptions, }, ] @@ -93,7 +95,7 @@ export function NotificationButtonRow({ ? [ { id: "folder", - label: "Folder", // @TODO localize + label: i18n.folder, options: folderOptions, }, ] diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index d2ac7f36277..0508991c5da 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -16,16 +16,18 @@ const { css } = createEmotion({ export type NotificationConfirmationBodyProps = { buttonText: string; + itemName: string; confirmationMessage: string; error?: string; messageDetails?: string; tasksAreComplete?: boolean; theme: Theme; - handleOpenVault: (e: Event) => void; + handleOpenVault: () => void; }; export function NotificationConfirmationBody({ buttonText, + itemName, confirmationMessage, error, messageDetails, @@ -43,6 +45,7 @@ export function NotificationConfirmationBody({ ${showConfirmationMessage ? NotificationConfirmationMessage({ buttonText, + itemName, message: confirmationMessage, messageDetails, theme, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts index a071338af9a..5cc977cf4cb 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -20,14 +20,14 @@ import { NotificationConfirmationFooter } from "./footer"; export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; + handleOpenVault: () => void; handleOpenTasks: (e: Event) => void; } & { error?: string; i18n: { [key: string]: string }; + itemName: string; task?: NotificationTaskInfo; type: NotificationType; - username: string; }; export function NotificationConfirmationContainer({ @@ -36,13 +36,13 @@ export function NotificationConfirmationContainer({ handleOpenVault, handleOpenTasks, i18n, + itemName, task, theme = ThemeTypes.Light, type, - username, }: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); - const confirmationMessage = getConfirmationMessage(i18n, username, type, error); + const confirmationMessage = getConfirmationMessage(i18n, itemName, type, error); const buttonText = error ? i18n.newItem : i18n.view; let messageDetails: string | undefined; @@ -71,6 +71,7 @@ export function NotificationConfirmationContainer({ })} ${NotificationConfirmationBody({ buttonText, + itemName, confirmationMessage, tasksAreComplete, messageDetails, @@ -106,19 +107,17 @@ const notificationContainerStyles = (theme: Theme) => css` function getConfirmationMessage( i18n: { [key: string]: string }, - username: string, + itemName: string, type?: NotificationType, error?: string, ) { - const loginSaveSuccessDetails = chrome.i18n.getMessage("loginSaveSuccessDetails", [username]); - const loginUpdatedSuccessDetails = chrome.i18n.getMessage("loginUpdatedSuccessDetails", [ - username, - ]); + const loginSaveConfirmation = chrome.i18n.getMessage("loginSaveConfirmation", [itemName]); + const loginUpdatedConfirmation = chrome.i18n.getMessage("loginUpdatedConfirmation", [itemName]); if (error) { return i18n.saveFailureDetails; } - return type === "add" ? loginSaveSuccessDetails : loginUpdatedSuccessDetails; + return type === "add" ? loginSaveConfirmation : loginUpdatedConfirmation; } function getHeaderMessage( diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index c018371caff..3707e628370 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -7,19 +7,23 @@ import { themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { buttonText?: string; + itemName: string; message?: string; messageDetails?: string; - handleClick: (e: Event) => void; + handleClick: () => void; theme: Theme; }; export function NotificationConfirmationMessage({ buttonText, + itemName, message, messageDetails, handleClick, theme, }: NotificationConfirmationMessageProps) { + const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]); + return html`
${message || buttonText @@ -35,6 +39,10 @@ export function NotificationConfirmationMessage({ title=${buttonText} class=${notificationConfirmationButtonTextStyles(theme)} @click=${handleClick} + @keydown=${(e: KeyboardEvent) => handleButtonKeyDown(e, handleClick)} + aria-label=${buttonAria} + tabindex="0" + role="button" > ${buttonText} @@ -81,3 +89,10 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` font-size: 14px; color: ${themes[theme].text.muted}; `; + +function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleClick(); + } +} diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index c29f58e116b..e1d098e3b09 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -59,6 +59,7 @@ export function NotificationContainer({ ciphers, notificationType: type, theme, + i18n, }) : null} ${NotificationFooter({ diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 8ed69a96ad9..58a87ebc678 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -38,6 +38,7 @@ export function NotificationFooter({ ? NotificationButtonRow({ folders, organizations, + i18n, primaryButton: { handlePrimaryButtonClick: handleSaveAction, text: primaryButtonText, diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index cbfeffcf2f4..9dd02b64154 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -33,7 +33,7 @@ type NotificationBarWindowMessage = { data?: { cipherId?: string; task?: NotificationTaskInfo; - username?: string; + itemName?: string; }; error?: string; initData?: NotificationBarIframeInitData; diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index d660790ee63..fce05913e5e 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -53,23 +53,26 @@ function getI18n() { return { appName: chrome.i18n.getMessage("appName"), close: chrome.i18n.getMessage("close"), + collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), - loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), + loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), - loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), + loginUpdateConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"), loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), newItem: chrome.i18n.getMessage("newItem"), never: chrome.i18n.getMessage("never"), + myVault: chrome.i18n.getMessage("myVault"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), notificationAddSave: chrome.i18n.getMessage("notificationAddSave"), notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), - notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"), + notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"), notificationEdit: chrome.i18n.getMessage("edit"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), + notificationViewAria: chrome.i18n.getMessage("notificationViewAria"), saveAction: chrome.i18n.getMessage("notificationAddSave"), saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"), saveFailure: chrome.i18n.getMessage("saveFailure"), @@ -78,6 +81,7 @@ function getI18n() { typeLogin: chrome.i18n.getMessage("typeLogin"), updateLoginAction: chrome.i18n.getMessage("updateLoginAction"), updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"), + vault: chrome.i18n.getMessage("vault"), view: chrome.i18n.getMessage("view"), }; } @@ -200,7 +204,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement; const changeButton = findElementById(changeTemplate, "change-save"); - changeButton.textContent = i18n.notificationChangeSave; + changeButton.textContent = i18n.notificationUpdate; const changeEditButton = findElementById(changeTemplate, "change-edit"); changeEditButton.textContent = i18n.notificationEdit; @@ -346,10 +350,9 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM ); } -function openViewVaultItemPopout(e: Event, cipherId: string) { - e.preventDefault(); +function openViewVaultItemPopout(cipherId: string) { sendPlatformMessage({ - command: "bgOpenVault", + command: "bgOpenViewVaultItemPopout", cipherId, }); } @@ -357,7 +360,7 @@ function openViewVaultItemPopout(e: Event, cipherId: string) { function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { const { theme, type } = notificationBarIframeInitData; const { error, data } = message; - const { username, cipherId, task } = data || {}; + const { cipherId, task, itemName } = data || {}; const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); @@ -371,9 +374,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { handleCloseNotification, i18n, error, - username: username ?? i18n.typeLogin, + itemName: itemName ?? i18n.typeLogin, task, - handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), + handleOpenVault: () => cipherId && openViewVaultItemPopout(cipherId), handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }), }), document.body, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 2b6827aafa4..3066ef5eef5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -191,6 +191,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DefaultEndUserNotificationService, + EndUserNotificationService, +} from "@bitwarden/common/vault/notifications"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, @@ -402,6 +406,7 @@ export default class MainBackground { sdkService: SdkService; sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; + endUserNotificationService: EndUserNotificationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; @@ -1320,6 +1325,14 @@ export default class MainBackground { this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); this.ipcService = new IpcBackgroundService(this.logService); + + this.endUserNotificationService = new DefaultEndUserNotificationService( + this.stateProvider, + this.apiService, + this.notificationsService, + this.authService, + this.logService, + ); } async bootstrap() { @@ -1406,6 +1419,9 @@ export default class MainBackground { this.taskService.listenForTaskNotifications(); } + if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) { + this.endUserNotificationService.listenForEndUserNotifications(); + } resolve(); }, 500); }); diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts index 9afc723825c..ac5331d3627 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts @@ -17,6 +17,8 @@ import { } from "@bitwarden/key-management"; import { UnlockOptions } from "@bitwarden/key-management-ui"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; import { ExtensionLockComponentService } from "./extension-lock-component.service"; @@ -117,6 +119,62 @@ describe("ExtensionLockComponentService", () => { }); }); + describe("popOutBrowserExtension", () => { + let openPopoutSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + openPopoutSpy = jest + .spyOn(BrowserPopupUtils, "openCurrentPagePopout") + .mockResolvedValue(undefined); + }); + + it("opens pop-out when the current window is neither a pop-out nor a sidebar", async () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(BrowserPopupUtils, "inSidebar").mockReturnValue(false); + + await service.popOutBrowserExtension(); + + expect(openPopoutSpy).toHaveBeenCalledWith(global.window); + }); + + test.each([ + [true, false], + [false, true], + [true, true], + ])("should not open pop-out under other conditions.", async (inPopout, inSidebar) => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(inPopout); + jest.spyOn(BrowserPopupUtils, "inSidebar").mockReturnValue(inSidebar); + + await service.popOutBrowserExtension(); + + expect(openPopoutSpy).not.toHaveBeenCalled(); + }); + }); + + describe("closeBrowserExtensionPopout", () => { + let closePopupSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + closePopupSpy = jest.spyOn(BrowserApi, "closePopup").mockReturnValue(); + }); + + it("closes pop-out when in pop-out", () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + + service.closeBrowserExtensionPopout(); + + expect(closePopupSpy).toHaveBeenCalledWith(global.window); + }); + + it("doesn't close pop-out when not in pop-out", () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + + service.closeBrowserExtensionPopout(); + + expect(closePopupSpy).not.toHaveBeenCalled(); + }); + }); + describe("isWindowVisible", () => { it("throws an error", async () => { await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts index 09a6f890e60..6ee1fc5175f 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts @@ -14,6 +14,8 @@ import { import { LockComponentService, UnlockOptions } from "@bitwarden/key-management-ui"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; export class ExtensionLockComponentService implements LockComponentService { @@ -37,6 +39,18 @@ export class ExtensionLockComponentService implements LockComponentService { return biometricsError.description; } + async popOutBrowserExtension(): Promise { + if (!BrowserPopupUtils.inPopout(global.window) && !BrowserPopupUtils.inSidebar(global.window)) { + await BrowserPopupUtils.openCurrentPagePopout(global.window); + } + } + + closeBrowserExtensionPopout(): void { + if (BrowserPopupUtils.inPopout(global.window)) { + BrowserApi.closePopup(global.window); + } + } + async isWindowVisible(): Promise { throw new Error("Method not implemented."); } diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 79412e6bce8..a4c6b894159 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -340,6 +340,7 @@ export default { generator: "Generator", send: "Send", settings: "Settings", + labelWithNotification: (label: string) => `${label}: New Notification`, }); }, }, @@ -398,17 +399,64 @@ export default { type Story = StoryObj; -export const PopupTabNavigation: Story = { +type PopupTabNavigationStory = StoryObj; + +const navButtons = (showBerry = false) => [ + { + label: "vault", + page: "/tabs/vault", + iconKey: "lock", + iconKeyActive: "lock-f", + }, + { + label: "generator", + page: "/tabs/generator", + iconKey: "generate", + iconKeyActive: "generate-f", + }, + { + label: "send", + page: "/tabs/send", + iconKey: "send", + iconKeyActive: "send-f", + }, + { + label: "settings", + page: "/tabs/settings", + iconKey: "cog", + iconKeyActive: "cog-f", + showBerry: showBerry, + }, +]; + +export const DefaultPopupTabNavigation: PopupTabNavigationStory = { render: (args) => ({ props: args, - template: /* HTML */ ` + template: /*html*/ ` - + - - `, + `, }), + args: { + navButtons: navButtons(), + }, +}; + +export const PopupTabNavigationWithBerry: PopupTabNavigationStory = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + `, + }), + args: { + navButtons: navButtons(true), + }, }; export const PopupPage: Story = { diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index bed4eac3f90..27b546738c3 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -5,12 +5,13 @@
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index e01b4efd71b..f4b82dc56fc 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -1,10 +1,19 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LinkModule } from "@bitwarden/components"; +export type NavButton = { + label: string; + page: string; + iconKey: string; + iconKeyActive: string; + showBerry?: boolean; +}; + @Component({ selector: "popup-tab-navigation", templateUrl: "popup-tab-navigation.component.html", @@ -15,30 +24,12 @@ import { LinkModule } from "@bitwarden/components"; }, }) export class PopupTabNavigationComponent { - navButtons = [ - { - label: "vault", - page: "/tabs/vault", - iconKey: "lock", - iconKeyActive: "lock-f", - }, - { - label: "generator", - page: "/tabs/generator", - iconKey: "generate", - iconKeyActive: "generate-f", - }, - { - label: "send", - page: "/tabs/send", - iconKey: "send", - iconKeyActive: "send-f", - }, - { - label: "settings", - page: "/tabs/settings", - iconKey: "cog", - iconKeyActive: "cog-f", - }, - ]; + @Input() navButtons: NavButton[] = []; + + constructor(private i18nService: I18nService) {} + + buttonTitle(navButton: NavButton) { + const labelText = this.i18nService.t(navButton.label); + return navButton.showBerry ? this.i18nService.t("labelWithNotification", labelText) : labelText; + } } diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index 457198eaa4e..6fc3e11493c 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -27,6 +27,7 @@ import { ClEAR_VIEW_CACHE_COMMAND, POPUP_VIEW_CACHE_KEY, SAVE_VIEW_CACHE_COMMAND, + ViewCacheState, } from "../../services/popup-view-cache-background.service"; /** @@ -42,8 +43,8 @@ export class PopupViewCacheService implements ViewCacheService { private messageSender = inject(MessageSender); private router = inject(Router); - private _cache: Record; - private get cache(): Record { + private _cache: Record; + private get cache(): Record { if (!this._cache) { throw new Error("Dirty View Cache not initialized"); } @@ -64,15 +65,9 @@ export class PopupViewCacheService implements ViewCacheService { filter((e) => e instanceof NavigationEnd), /** Skip the first navigation triggered by `popupRouterCacheGuard` */ skip(1), - filter((e: NavigationEnd) => - // viewing/editing a cipher and navigating back to the vault list should not clear the cache - ["/view-cipher", "/edit-cipher", "/tabs/vault"].every( - (route) => !e.urlAfterRedirects.startsWith(route), - ), - ), ) - .subscribe((e) => { - return this.clearState(); + .subscribe(() => { + return this.clearState(true); }); } @@ -85,13 +80,20 @@ export class PopupViewCacheService implements ViewCacheService { key, injector = inject(Injector), initialValue, + persistNavigation, } = options; - const cachedValue = this.cache[key] ? deserializer(JSON.parse(this.cache[key])) : initialValue; + const cachedValue = this.cache[key] + ? deserializer(JSON.parse(this.cache[key].value)) + : initialValue; const _signal = signal(cachedValue); + const viewCacheOptions = { + ...(persistNavigation && { persistNavigation }), + }; + effect( () => { - this.updateState(key, JSON.stringify(_signal())); + this.updateState(key, JSON.stringify(_signal()), viewCacheOptions); }, { injector }, ); @@ -123,15 +125,24 @@ export class PopupViewCacheService implements ViewCacheService { return control; } - private updateState(key: string, value: string) { + private updateState(key: string, value: string, options: ViewCacheState["options"]) { this.messageSender.send(SAVE_VIEW_CACHE_COMMAND, { key, value, + options, }); } - private clearState() { - this._cache = {}; // clear local cache - this.messageSender.send(ClEAR_VIEW_CACHE_COMMAND, {}); + private clearState(routeChange: boolean = false) { + if (routeChange) { + // Only keep entries with `persistNavigation` + this._cache = Object.fromEntries( + Object.entries(this._cache).filter(([, { options }]) => options?.persistNavigation), + ); + } else { + // Clear all entries + this._cache = {}; + } + this.messageSender.send(ClEAR_VIEW_CACHE_COMMAND, { routeChange: routeChange }); } } diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts index b6009c4cc2e..2ec75791d1b 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts @@ -14,6 +14,7 @@ import { ClEAR_VIEW_CACHE_COMMAND, POPUP_VIEW_CACHE_KEY, SAVE_VIEW_CACHE_COMMAND, + ViewCacheState, } from "../../services/popup-view-cache-background.service"; import { PopupViewCacheService } from "./popup-view-cache.service"; @@ -35,6 +36,7 @@ export class TestComponent { signal = this.viewCacheService.signal({ key: "test-signal", initialValue: "initial signal", + persistNavigation: true, }); } @@ -42,11 +44,11 @@ describe("popup view cache", () => { const configServiceMock = mock(); let testBed: TestBed; let service: PopupViewCacheService; - let fakeGlobalState: FakeGlobalState>; + let fakeGlobalState: FakeGlobalState>; let messageSenderMock: MockProxy; let router: Router; - const initServiceWithState = async (state: Record) => { + const initServiceWithState = async (state: Record) => { await fakeGlobalState.update(() => state); await service.init(); }; @@ -106,7 +108,11 @@ describe("popup view cache", () => { }); it("should initialize signal from state", async () => { - await initServiceWithState({ "foo-123": JSON.stringify("bar") }); + await initServiceWithState({ + "foo-123": { + value: JSON.stringify("bar"), + }, + }); const injector = TestBed.inject(Injector); @@ -120,7 +126,11 @@ describe("popup view cache", () => { }); it("should initialize form from state", async () => { - await initServiceWithState({ "test-form-cache": JSON.stringify({ name: "baz" }) }); + await initServiceWithState({ + "test-form-cache": { + value: JSON.stringify({ name: "baz" }), + }, + }); const fixture = TestBed.createComponent(TestComponent); const component = fixture.componentRef.instance; @@ -138,7 +148,11 @@ describe("popup view cache", () => { }); it("should utilize deserializer", async () => { - await initServiceWithState({ "foo-123": JSON.stringify("bar") }); + await initServiceWithState({ + "foo-123": { + value: JSON.stringify("bar"), + }, + }); const injector = TestBed.inject(Injector); @@ -178,6 +192,9 @@ describe("popup view cache", () => { expect(messageSenderMock.send).toHaveBeenCalledWith(SAVE_VIEW_CACHE_COMMAND, { key: "test-signal", value: JSON.stringify("Foobar"), + options: { + persistNavigation: true, + }, }); }); @@ -192,18 +209,63 @@ describe("popup view cache", () => { expect(messageSenderMock.send).toHaveBeenCalledWith(SAVE_VIEW_CACHE_COMMAND, { key: "test-form-cache", value: JSON.stringify({ name: "Foobar" }), + options: {}, }); }); it("should clear on 2nd navigation", async () => { - await initServiceWithState({ temp: "state" }); + await initServiceWithState({ + temp: { + value: "state", + options: {}, + }, + }); await router.navigate(["a"]); expect(messageSenderMock.send).toHaveBeenCalledTimes(0); - expect(service["_cache"]).toEqual({ temp: "state" }); + expect(service["_cache"]).toEqual({ + temp: { + value: "state", + options: {}, + }, + }); await router.navigate(["b"]); - expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, {}); + expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, { + routeChange: true, + }); expect(service["_cache"]).toEqual({}); }); + + it("should respect persistNavigation setting on 2nd navigation", async () => { + await initServiceWithState({ + keepState: { + value: "state", + options: { + persistNavigation: true, + }, + }, + removeState: { + value: "state", + options: { + persistNavigation: false, + }, + }, + }); + + await router.navigate(["a"]); // first navigation covered in previous test + + await router.navigate(["b"]); + expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, { + routeChange: true, + }); + expect(service["_cache"]).toEqual({ + keepState: { + value: "state", + options: { + persistNavigation: true, + }, + }, + }); + }); }); diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts index 98a6065189b..79c04e90aad 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -16,8 +16,27 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; const popupClosedPortName = "new_popup"; +export type ViewCacheOptions = { + /** + * Optional flag to persist the cached value between navigation events. + */ + persistNavigation?: boolean; +}; + +export type ViewCacheState = { + /** + * The cached value + */ + value: string; // JSON value + + /** + * Options for managing/clearing the cache + */ + options?: ViewCacheOptions; +}; + /** We cannot use `UserKeyDefinition` because we must be able to store state when there is no active user. */ -export const POPUP_VIEW_CACHE_KEY = KeyDefinition.record( +export const POPUP_VIEW_CACHE_KEY = KeyDefinition.record( POPUP_VIEW_MEMORY, "popup-view-cache", { @@ -36,9 +55,15 @@ export const POPUP_ROUTE_HISTORY_KEY = new KeyDefinition( export const SAVE_VIEW_CACHE_COMMAND = new CommandDefinition<{ key: string; value: string; + options: ViewCacheOptions; }>("save-view-cache"); -export const ClEAR_VIEW_CACHE_COMMAND = new CommandDefinition("clear-view-cache"); +export const ClEAR_VIEW_CACHE_COMMAND = new CommandDefinition<{ + /** + * Flag to indicate the clear request was triggered by a route change in popup. + */ + routeChange: boolean; +}>("clear-view-cache"); export class PopupViewCacheBackgroundService { private popupViewCacheState = this.globalStateProvider.get(POPUP_VIEW_CACHE_KEY); @@ -61,10 +86,13 @@ export class PopupViewCacheBackgroundService { this.messageListener .messages$(SAVE_VIEW_CACHE_COMMAND) .pipe( - concatMap(async ({ key, value }) => + concatMap(async ({ key, value, options }) => this.popupViewCacheState.update((state) => ({ ...state, - [key]: value, + [key]: { + value, + options, + }, })), ), ) @@ -72,7 +100,19 @@ export class PopupViewCacheBackgroundService { this.messageListener .messages$(ClEAR_VIEW_CACHE_COMMAND) - .pipe(concatMap(() => this.popupViewCacheState.update(() => null))) + .pipe( + concatMap(({ routeChange }) => + this.popupViewCacheState.update((state) => { + if (routeChange && state) { + // Only remove keys that are not marked with `persistNavigation` + return Object.fromEntries( + Object.entries(state).filter(([, { options }]) => options?.persistNavigation), + ); + } + return null; + }), + ), + ) .subscribe(); // on popup closed, with 2 minute delay that is cancelled by re-opening the popup diff --git a/apps/browser/src/popup/tabs-v2.component.html b/apps/browser/src/popup/tabs-v2.component.html new file mode 100644 index 00000000000..bde3aaa3d31 --- /dev/null +++ b/apps/browser/src/popup/tabs-v2.component.html @@ -0,0 +1,3 @@ + + + diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index 4cdb8fc029d..1392dc565ab 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -1,11 +1,53 @@ import { Component } from "@angular/core"; +import { combineLatest, map } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { HasNudgeService } from "@bitwarden/vault"; @Component({ selector: "app-tabs-v2", - template: ` - - - - `, + templateUrl: "./tabs-v2.component.html", + providers: [HasNudgeService], }) -export class TabsV2Component {} +export class TabsV2Component { + constructor( + private readonly hasNudgeService: HasNudgeService, + private readonly configService: ConfigService, + ) {} + + protected navButtons$ = combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), + this.hasNudgeService.shouldShowNudge$(), + ]).pipe( + map(([onboardingFeatureEnabled, showNudge]) => { + return [ + { + label: "vault", + page: "/tabs/vault", + iconKey: "lock", + iconKeyActive: "lock-f", + }, + { + label: "generator", + page: "/tabs/generator", + iconKey: "generate", + iconKeyActive: "generate-f", + }, + { + label: "send", + page: "/tabs/send", + iconKey: "send", + iconKeyActive: "send-f", + }, + { + label: "settings", + page: "/tabs/settings", + iconKey: "cog", + iconKeyActive: "cog-f", + showBerry: onboardingFeatureEnabled && showNudge, + }, + ]; + }), + ); +} 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 25bf3ce3716..ff583061684 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 @@ -12,10 +12,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { EndUserNotificationService } from "@bitwarden/common/vault/notifications"; +import { NotificationView } from "@bitwarden/common/vault/notifications/models"; import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; import { @@ -66,6 +69,7 @@ describe("AtRiskPasswordsComponent", () => { let mockTasks$: BehaviorSubject; let mockCiphers$: BehaviorSubject; let mockOrgs$: BehaviorSubject; + let mockNotifications$: BehaviorSubject; let mockInlineMenuVisibility$: BehaviorSubject; let calloutDismissed$: BehaviorSubject; const setInlineMenuVisibility = jest.fn(); @@ -73,6 +77,7 @@ describe("AtRiskPasswordsComponent", () => { const mockAtRiskPasswordPageService = mock(); const mockChangeLoginPasswordService = mock(); const mockDialogService = mock(); + const mockConfigService = mock(); beforeEach(async () => { mockTasks$ = new BehaviorSubject([ @@ -101,6 +106,7 @@ describe("AtRiskPasswordsComponent", () => { name: "Org 1", } as Organization, ]); + mockNotifications$ = new BehaviorSubject([]); mockInlineMenuVisibility$ = new BehaviorSubject( AutofillOverlayVisibility.Off, @@ -110,6 +116,7 @@ describe("AtRiskPasswordsComponent", () => { setInlineMenuVisibility.mockClear(); mockToastService.showToast.mockClear(); mockDialogService.open.mockClear(); + mockConfigService.getFeatureFlag.mockClear(); mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$); await TestBed.configureTestingModule({ @@ -133,6 +140,12 @@ describe("AtRiskPasswordsComponent", () => { cipherViews$: () => mockCiphers$, }, }, + { + provide: EndUserNotificationService, + useValue: { + unreadNotifications$: () => mockNotifications$, + }, + }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: AccountService, useValue: { activeAccount$: of({ id: "user" }) } }, { provide: PlatformUtilsService, useValue: mock() }, @@ -145,6 +158,7 @@ describe("AtRiskPasswordsComponent", () => { }, }, { provide: ToastService, useValue: mockToastService }, + { provide: ConfigService, useValue: mockConfigService }, ], }) .overrideModule(JslibModule, { 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 37c445f6c30..1b43151193a 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 @@ -1,7 +1,19 @@ import { CommonModule } from "@angular/common"; -import { Component, inject, OnInit, signal } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; -import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs"; +import { + combineLatest, + concat, + concatMap, + firstValueFrom, + map, + of, + shareReplay, + startWith, + switchMap, + take, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -11,10 +23,13 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { EndUserNotificationService } from "@bitwarden/common/vault/notifications"; import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { @@ -81,6 +96,9 @@ export class AtRiskPasswordsComponent implements OnInit { private changeLoginPasswordService = inject(ChangeLoginPasswordService); private platformUtilsService = inject(PlatformUtilsService); private dialogService = inject(DialogService); + private endUserNotificationService = inject(EndUserNotificationService); + private configService = inject(ConfigService); + private destroyRef = inject(DestroyRef); /** * The cipher that is currently being launched. Used to show a loading spinner on the badge button. @@ -180,6 +198,36 @@ export class AtRiskPasswordsComponent implements OnInit { await this.atRiskPasswordPageService.dismissGettingStarted(userId); } } + + if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) { + this.markTaskNotificationsAsRead(); + } + } + + private markTaskNotificationsAsRead() { + this.activeUserData$ + .pipe( + switchMap(({ tasks, userId }) => { + return this.endUserNotificationService.unreadNotifications$(userId).pipe( + take(1), + map((notifications) => { + return notifications.filter((notification) => { + return tasks.some((task) => task.id === notification.taskId); + }); + }), + concatMap((unreadTaskNotifications) => { + // TODO: Investigate creating a bulk endpoint to mark notifications as read + return concat( + ...unreadTaskNotifications.map((n) => + this.endUserNotificationService.markAsRead(n.id, userId), + ), + ); + }), + ); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); } async viewCipher(cipher: CipherView) { diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts index 5e400da9de5..ae6369d06a5 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { inject } from "@angular/core"; import { Router } from "@angular/router"; 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 5f2ec858ed6..b4cf79e7422 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 @@ -50,6 +50,7 @@ export class VaultPopupItemsService { private cachedSearchText = inject(PopupViewCacheService).signal({ key: "vault-search-text", initialValue: "", + persistNavigation: true, }); readonly searchText$ = toObservable(this.cachedSearchText); 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 6cce5796cbe..f11fa0f63f0 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 @@ -188,6 +188,7 @@ export class VaultPopupListFiltersService { key: "vault-filters", initialValue: {}, deserializer: (v) => v, + persistNavigation: true, }); this.deserializeFilters(cachedFilters()); diff --git a/apps/browser/store/locales/ar/copy.resx b/apps/browser/store/locales/ar/copy.resx index 9fdfb942100..a83bafbf1ae 100644 --- a/apps/browser/store/locales/ar/copy.resx +++ b/apps/browser/store/locales/ar/copy.resx @@ -118,58 +118,60 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + مدير كلمات المرور بتواردن - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + في المنزل، في العمل، أو في أثناء التنقل، يقوم بتواردن بتأمين جميع كلمات المرور والمعلومات الحساسة بسهولة. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + مُعترف به كأفضل مدير كلمات مرور من قِبل PCMag وWIRED وThe Verge وCNET وG2 وغيرها! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +أمّن حياتك الرقمية -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +أمّن حياتك الرقمية واحمِ بياناتك من الاختراقات بإنشاء كلمات مرور فريدة وقوية وحفظها لكل حساب. احفظ كل شيء في مخزن كلمات مرور مشفّر من البداية إلى النهاية، لا يمكن لأحد الوصول إليه سواك. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +الوصول إلى بياناتك، من أي مكان، وفي أي وقت، وعلى أي جهاز -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +يمكنك بسهولة إدارة كلمات مرور غير محدودة وتخزينها وتأمينها ومشاركتها عبر عدد غير محدود من الأجهزة دون قيود. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +يجب أن يمتلك الجميع الأدوات اللازمة للبقاء آمنًا على الإنترنت +استخدم بيتواردن مجانًا دون إعلانات أو بيع بيانات. تؤمن بيتواردن بحق الجميع في البقاء آمنًا على الإنترنت. توفر الباقات المميزة إمكانية الوصول إلى ميزات متقدمة. -More reasons to choose Bitwarden: +عزز قدرات فرقك مع بتواردن +تأتي باقاتنا للفرق والمؤسسات مزودة بميزات احترافية للأعمال. من الأمثلة على ذلك تكامل SSO، والاستضافة الذاتية، وتكامل الدليل، وتوفير SCIM، والسياسات العالمية، والوصول إلى واجهة برمجة التطبيقات، وسجلات الأحداث، والمزيد. -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +استخدم بيتواردن لتأمين فريق عملك ومشاركة المعلومات الحساسة مع زملائك. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +أسباب إضافية لاختيار بتواردن: -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +تشفير عالمي المستوى +كلمات المرور محمية بتشفير متقدم من البداية إلى النهاية (AES-256 بت، وهاشتاج مُملح، وPBKDF2 SHA-256) لضمان أمان بياناتك وخصوصيتها. -Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +عمليات تدقيق خارجية +تُجري بيتواردن بانتظام عمليات تدقيق أمنية شاملة من جهات خارجية بالتعاون مع شركات أمنية مرموقة. تشمل هذه العمليات السنوية تقييمات لشفرة المصدر واختبارات اختراق عبر عناوين IP وخوادم وتطبيقات الويب الخاصة بـبتواردن. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +مصادقة ثنائية متقدمة +أمّن تسجيل دخولك باستخدام مُصادق خارجي، أو رموز مُرسلة عبر البريد الإلكتروني، أو بيانات اعتماد FIDO2 WebAuthn مثل مفتاح أمان الأجهزة أو كلمة المرور. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +إرسال بتواردن +انقل البيانات مباشرةً إلى الآخرين مع الحفاظ على أمان مشفّر من البداية إلى النهاية والحد من التعرض. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +مولد مدمج +أنشئ كلمات مرور طويلة ومعقدة ومميزة وأسماء مستخدمين فريدة لكل موقع تزوره. تكامل مع مزودي أسماء البريد الإلكتروني المستعارة لمزيد من الخصوصية. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +ترجمات عالمية +تتوفر ترجمات بتواردن لأكثر من 60 لغة، مترجمة من قِبل المجتمع العالمي عبر Crowdin. + +تطبيقات متعددة المنصات +أمّن بياناتك الحساسة وشاركها داخل مخزن بتواردن من أي متصفح أو جهاز محمول أو نظام تشغيل سطح مكتب، وغير ذلك الكثير. + +يؤمن بتواردن أكثر من مجرد كلمات مرور +تُمكّن حلول إدارة بيانات الاعتماد المشفرة من البداية إلى النهاية من بتواردن المؤسسات من تأمين كل شيء، بما في ذلك أسرار المطورين وتجارب مفاتيح المرور. تفضل بزيارة Bitwarden.com لمعرفة المزيد عن المدير السري لبتواردن وBitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + في المنزل، في العمل، أو في أثناء التنقل، يقوم بتواردن بتأمين جميع كلمات المرور والمعلومات الحساسة بسهولة. مزامنة خزانتك والوصول إليها من عدة أجهزة diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 30d87c5c662..925706bec7b 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -33,7 +33,7 @@ "gatekeeperAssess": false, "hardenedRuntime": true, "entitlements": "resources/entitlements.mac.plist", - "entitlementsInherit": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.inherit.plist", "extendInfo": { "ITSAppUsesNonExemptEncryption": false, "CFBundleLocalizations": [ @@ -67,6 +67,7 @@ ], "CFBundleDevelopmentRegion": "en" }, + "provisioningProfile": "bitwarden_desktop_developer_id.provisionprofile", "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "extraFiles": [ { @@ -78,7 +79,11 @@ "to": "MacOS/desktop_proxy.inherit" } ], - "signIgnore": ["MacOS/desktop_proxy", "MacOS/desktop_proxy.inherit"], + "signIgnore": [ + "MacOS/desktop_proxy", + "MacOS/desktop_proxy.inherit", + "Contents/Plugins/autofill-extension.appex" + ], "target": ["dmg", "zip"] }, "win": { @@ -137,7 +142,8 @@ "extendInfo": { "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" - } + }, + "provisioningProfile": "bitwarden_desktop_appstore.provisionprofile" }, "nsisWeb": { "oneClick": false, diff --git a/apps/desktop/macos/Debug.xcconfig b/apps/desktop/macos/Debug.xcconfig new file mode 100644 index 00000000000..73d8cd871fb --- /dev/null +++ b/apps/desktop/macos/Debug.xcconfig @@ -0,0 +1,11 @@ +// +// Debug.xcconfig +// desktop +// +// Created by Nathan Ansel on 2/20/25. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 +CODE_SIGN_IDENTITY = Apple Development +PROVISIONING_PROFILE_SPECIFIER = Bitwarden Desktop Autofill Development 2024 diff --git a/apps/desktop/macos/ReleaseAppStore.xcconfig b/apps/desktop/macos/ReleaseAppStore.xcconfig new file mode 100644 index 00000000000..2b891bbfc81 --- /dev/null +++ b/apps/desktop/macos/ReleaseAppStore.xcconfig @@ -0,0 +1,11 @@ +// +// ReleaseAppStore.xcconfig +// desktop +// +// Created by Vince Grassia on 7/25/24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 +CODE_SIGN_IDENTITY = 3rd Party Mac Developer Application +PROVISIONING_PROFILE_SPECIFIER = Bitwarden Desktop Autofill App Store 2024 diff --git a/apps/desktop/macos/ReleaseDeveloper.xcconfig b/apps/desktop/macos/ReleaseDeveloper.xcconfig new file mode 100644 index 00000000000..47a047cbcf3 --- /dev/null +++ b/apps/desktop/macos/ReleaseDeveloper.xcconfig @@ -0,0 +1,11 @@ +// +// ReleaseDeveloper.xcconfig +// desktop +// +// Created by Nathan Ansel on 2/20/25. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 +CODE_SIGN_IDENTITY = Developer ID Application +PROVISIONING_PROFILE_SPECIFIER = Bitwarden Desktop Autofill Extension Developer Dis diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj index 2ac467f3289..ff257097f26 100644 --- a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -17,7 +17,9 @@ /* Begin PBXFileReference section */ 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = ""; }; 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = ""; }; - 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; + 968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseAppStore.xcconfig; sourceTree = ""; }; + D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseDeveloper.xcconfig; sourceTree = ""; }; E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProviderViewController.swift; sourceTree = ""; }; @@ -42,7 +44,9 @@ E1DF711D2B342E2800F29026 = { isa = PBXGroup; children = ( - 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */, + D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */, + 968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */, + D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */, E1DF71402B342F6900F29026 /* autofill-extension */, E1DF713D2B342F6900F29026 /* Frameworks */, E1DF71272B342E2800F29026 /* Products */, @@ -166,8 +170,97 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + D83832AE2D67BA84003FB9F8 /* ReleaseDeveloper */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = ReleaseDeveloper; + }; + D83832AF2D67BA84003FB9F8 /* ReleaseDeveloper */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "autofill-extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Bitwarden Desktop Autofill App Store 2024"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseDeveloper; + }; E1DF71332B342E2900F29026 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -223,15 +316,16 @@ MACOSX_DEPLOYMENT_TARGET = 14.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; + ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; - E1DF71342B342E2900F29026 /* Release */ = { + E1DF71342B342E2900F29026 /* ReleaseAppStore */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -284,10 +378,11 @@ SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; }; - name = Release; + name = ReleaseAppStore; }; E1DF714C2B342F6900F29026 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; @@ -309,16 +404,16 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024"; + PROVISIONING_PROFILE_SPECIFIER = "Bitwarden Desktop Autofill Development 2024"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; - E1DF714D2B342F6900F29026 /* Release */ = { + E1DF714D2B342F6900F29026 /* ReleaseAppStore */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; @@ -340,13 +435,12 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024"; + PROVISIONING_PROFILE_SPECIFIER = "Bitwarden Desktop Autofill App Store 2024"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; - name = Release; + name = ReleaseAppStore; }; /* End XCBuildConfiguration section */ @@ -355,19 +449,21 @@ isa = XCConfigurationList; buildConfigurations = ( E1DF71332B342E2900F29026 /* Debug */, - E1DF71342B342E2900F29026 /* Release */, + E1DF71342B342E2900F29026 /* ReleaseAppStore */, + D83832AE2D67BA84003FB9F8 /* ReleaseDeveloper */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = ReleaseAppStore; }; E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */ = { isa = XCConfigurationList; buildConfigurations = ( E1DF714C2B342F6900F29026 /* Debug */, - E1DF714D2B342F6900F29026 /* Release */, + E1DF714D2B342F6900F29026 /* ReleaseAppStore */, + D83832AF2D67BA84003FB9F8 /* ReleaseDeveloper */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = ReleaseAppStore; }; /* End XCConfigurationList section */ }; diff --git a/apps/desktop/macos/production.xcconfig b/apps/desktop/macos/production.xcconfig deleted file mode 100644 index f06f2bf736e..00000000000 --- a/apps/desktop/macos/production.xcconfig +++ /dev/null @@ -1,11 +0,0 @@ -// -// Production.xcconfig -// desktop -// -// Created by Vince Grassia on 7/25/24. -// - -// Configuration settings file format documentation can be found at: -// https://help.apple.com/xcode/#/dev745c5c974 -CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application -PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = Bitwarden Desktop Autofill App Store 2024 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9f442da47a1..e00df0b26df 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,7 +23,9 @@ "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", "build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", - "build:macos-extension": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js", + "build:macos-extension:mac": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mac", + "build:macos-extension:mas": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas", + "build:macos-extension:masdev": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas-dev", "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", @@ -38,17 +40,21 @@ "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/", "pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", + "pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", + "pack:mac:mas:with-extension": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never", "pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never", - "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension && electron-builder --mac mas-dev --universal -p never", + "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never", "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"", "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", "dist:lin": "npm run build && npm run pack:lin", "dist:lin:arm64": "npm run build && npm run pack:lin:arm64", "dist:mac": "npm run build && npm run pack:mac", + "dist:mac:with-extension": "npm run build && npm run pack:mac:with-extension", "dist:mac:mas": "npm run build && npm run pack:mac:mas", + "dist:mac:mas:with-extension": "npm run build && npm run pack:mac:mas:with-extension", "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", "dist:mac:masdev:with-extension": "npm run build && npm run pack:mac:masdev:with-extension", "dist:win": "npm run build && npm run pack:win", diff --git a/apps/desktop/resources/entitlements.mac.inherit.plist b/apps/desktop/resources/entitlements.mac.inherit.plist new file mode 100644 index 00000000000..d35e43ae588 --- /dev/null +++ b/apps/desktop/resources/entitlements.mac.inherit.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist index e273bcc7eca..fe49256d71c 100644 --- a/apps/desktop/resources/entitlements.mac.plist +++ b/apps/desktop/resources/entitlements.mac.plist @@ -2,11 +2,13 @@ - com.apple.security.cs.allow-jit - - + com.apple.security.cs.allow-jit + diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist index 7e957fce7ce..fca5f02d52d 100644 --- a/apps/desktop/resources/entitlements.mas.inherit.plist +++ b/apps/desktop/resources/entitlements.mas.inherit.plist @@ -8,9 +8,5 @@ com.apple.security.cs.allow-jit - diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index f3bc20011ff..3ebd56f0fd7 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -6,6 +6,8 @@ LTZ2PFU5D6.com.bitwarden.desktop com.apple.developer.team-identifier LTZ2PFU5D6 + com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.app-sandbox com.apple.security.application-groups @@ -18,10 +20,6 @@ com.apple.security.device.usb - com.apple.security.temporary-exception.files.home-relative-path.read-write /Library/Application Support/Mozilla/NativeMessagingHosts/ diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js index 20c24c8a76b..7c9ad381dc2 100644 --- a/apps/desktop/scripts/after-sign.js +++ b/apps/desktop/scripts/after-sign.js @@ -16,7 +16,7 @@ async function run(context) { const appPath = `${context.appOutDir}/${appName}.app`; const macBuild = context.electronPlatformName === "darwin"; const copySafariExtension = ["darwin", "mas"].includes(context.electronPlatformName); - const copyAutofillExtension = ["mas"].includes(context.electronPlatformName); + const copyAutofillExtension = ["darwin", "mas"].includes(context.electronPlatformName); let shouldResign = false; diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js index 649fe3b6736..dcc1725d50f 100644 --- a/apps/desktop/scripts/build-macos-extension.js +++ b/apps/desktop/scripts/build-macos-extension.js @@ -6,14 +6,19 @@ const fse = require("fs-extra"); const paths = { macosBuild: "./macos/build", - extensionBuild: "./macos/build/Release/autofill-extension.appex", + extensionBuildDebug: "./macos/build/Debug/autofill-extension.appex", + extensionBuildReleaseAppStore: "./macos/build/ReleaseAppStore/autofill-extension.appex", + extensionBuildReleaseDeveloper: "./macos/build/ReleaseDeveloper/autofill-extension.appex", extensionDistDir: "./macos/dist", extensionDist: "./macos/dist/autofill-extension.appex", macOsProject: "./macos/desktop.xcodeproj", - macOsConfig: "./macos/production.xcconfig", }; +exports.default = buildMacOs; + async function buildMacOs() { + console.log("### Building Autofill Extension"); + if (fse.existsSync(paths.macosBuild)) { fse.removeSync(paths.macosBuild); } @@ -22,15 +27,50 @@ async function buildMacOs() { fse.removeSync(paths.extensionDistDir); } + let configuration; + let codeSignIdentity; + let provisioningProfileSpecifier; + let buildDirectory; + const configurationArgument = process.argv[2]; + if (configurationArgument !== undefined) { + // Use the configuration passed in to determine the configuration file. + if (configurationArgument == "mas-dev") { + configuration = "Debug"; + codeSignIdentity = "Apple Development"; + provisioningProfileSpecifier = "Bitwarden Desktop Autofill Development 2024"; + buildDirectory = paths.extensionBuildDebug; + } else if (configurationArgument == "mas") { + configuration = "ReleaseAppStore"; + codeSignIdentity = "3rd Party Mac Developer Application"; + provisioningProfileSpecifier = "Bitwarden Desktop Autofill App Store 2024"; + buildDirectory = paths.extensionBuildReleaseAppStore; + } else if (configurationArgument == "mac") { + configuration = "ReleaseDeveloper"; + codeSignIdentity = "Developer ID Application"; + provisioningProfileSpecifier = "Bitwarden Desktop Autofill Extension Developer Dis"; + buildDirectory = paths.extensionBuildReleaseDeveloper; + } else { + console.log("### Unable to determine configuration, skipping Autofill Extension build"); + return; + } + } else { + console.log("### No configuration argument found, skipping Autofill Extension build"); + return; + } + const proc = child.spawn("xcodebuild", [ "-project", paths.macOsProject, "-alltargets", "-configuration", - "Release", - // Uncomment when signing is fixed - // "-xcconfig", - // paths.macOsConfig, + configuration, + "CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO", + "OTHER_CODE_SIGN_FLAGS='--timestamp'", + + // While these arguments are defined in the `configuration` file above, xcodebuild has a bug in it currently that requires these arguments + // be explicitly defined in this call. + `CODE_SIGN_IDENTITY=${codeSignIdentity}`, + `PROVISIONING_PROFILE_SPECIFIER=${provisioningProfileSpecifier}`, ]); stdOutProc(proc); await new Promise((resolve, reject) => @@ -45,7 +85,8 @@ async function buildMacOs() { ); fse.mkdirSync(paths.extensionDistDir); - fse.copySync(paths.extensionBuild, paths.extensionDist); + fse.copySync(buildDirectory, paths.extensionDist); + // Delete the build dir, otherwise MacOS will load the extension from there instead of the Bitwarden.app bundle fse.removeSync(paths.macosBuild); } diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 0c6bc730c2c..00463152a95 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -41,6 +42,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, @@ -53,6 +55,7 @@ import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; +import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; @@ -132,11 +135,15 @@ const routes: Routes = [ }, ], }, - { - path: "vault", - component: VaultComponent, - canActivate: [authGuard, NewDeviceVerificationNoticeGuard], - }, + ...featureFlaggedRoute({ + defaultComponent: VaultComponent, + flaggedComponent: VaultV2Component, + featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm, + routeOptions: { + path: "vault", + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], + }, + }), { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, { @@ -359,7 +366,7 @@ const routes: Routes = [ imports: [ RouterModule.forRoot(routes, { useHash: true, - /*enableTracing: true,*/ + // enableTracing: true, }), ], exports: [RouterModule], diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index b892324a979..15ab4350bbc 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -4,11 +4,11 @@ import "zone.js"; import "../platform/app/locales"; import { NgModule } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { CalloutModule, DialogModule } from "@bitwarden/components"; -import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; @@ -27,6 +27,7 @@ import { PasswordHistoryComponent } from "../vault/app/vault/password-history.co import { ShareComponent } from "../vault/app/vault/share.component"; import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module"; import { VaultItemsComponent } from "../vault/app/vault/vault-items.component"; +import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/app/vault/view.component"; @@ -44,6 +45,8 @@ import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ + BrowserAnimationsModule, + SharedModule, AppRoutingModule, VaultFilterModule, @@ -52,8 +55,8 @@ import { SharedModule } from "./shared/shared.module"; CalloutModule, DeleteAccountComponent, UserVerificationComponent, - DecryptionFailureDialogComponent, NavComponent, + VaultV2Component, ], declarations: [ AccessibilityCookieComponent, @@ -62,7 +65,6 @@ import { SharedModule } from "./shared/shared.module"; AddEditCustomFieldsComponent, AppComponent, AttachmentsComponent, - VaultItemsComponent, CollectionsComponent, ColorPasswordPipe, ColorPasswordCountPipe, @@ -77,9 +79,10 @@ import { SharedModule } from "./shared/shared.module"; ShareComponent, UpdateTempPasswordComponent, VaultComponent, + VaultItemsComponent, VaultTimeoutInputComponent, - ViewComponent, ViewCustomFieldsComponent, + ViewComponent, ], providers: [SshAgentService], bootstrap: [AppComponent], diff --git a/apps/desktop/src/app/shared/shared.module.ts b/apps/desktop/src/app/shared/shared.module.ts index e08d29c4d12..6eed4a197f3 100644 --- a/apps/desktop/src/app/shared/shared.module.ts +++ b/apps/desktop/src/app/shared/shared.module.ts @@ -2,11 +2,9 @@ import { A11yModule } from "@angular/cdk/a11y"; import { DragDropModule } from "@angular/cdk/drag-drop"; import { OverlayModule } from "@angular/cdk/overlay"; import { ScrollingModule } from "@angular/cdk/scrolling"; -import { DatePipe } from "@angular/common"; +import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -15,9 +13,8 @@ import { ServicesModule } from "../services/services.module"; @NgModule({ imports: [ + CommonModule, A11yModule, - BrowserAnimationsModule, - BrowserModule, DragDropModule, FormsModule, JslibModule, @@ -28,9 +25,8 @@ import { ServicesModule } from "../services/services.module"; ], declarations: [AvatarComponent], exports: [ + CommonModule, A11yModule, - BrowserAnimationsModule, - BrowserModule, DatePipe, DragDropModule, FormsModule, diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts index 88dd8c60ed5..09a4dcef4b3 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts @@ -300,7 +300,7 @@ describe("MainBiometricsService", function () { expect(userKey).not.toBeNull(); expect(userKey!.keyB64).toBe(biometricKey); - expect(userKey!.encType).toBe(EncryptionType.AesCbc256_HmacSha256_B64); + expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( "Bitwarden_biometric", `${userId}_user_biometric`, diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts index cd8c94329bc..53647549295 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts @@ -1,5 +1,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { biometrics, passwords } from "@bitwarden/desktop-napi"; @@ -218,7 +220,13 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { symmetricKey: SymmetricCryptoKey, clientKeyPartB64: string | undefined, ): biometrics.KeyMaterial { - const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64; + let key = null; + const innerKey = symmetricKey.inner(); + if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + key = Utils.fromBufferToB64(innerKey.authenticationKey); + } else { + key = Utils.fromBufferToB64(innerKey.encryptionKey); + } const result = { osKeyPartB64: key, diff --git a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts index 6bfbc803e87..6772af4f905 100644 --- a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts +++ b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts @@ -104,6 +104,22 @@ describe("DesktopLockComponentService", () => { }); }); + describe("popOutBrowserExtension", () => { + it("throws platform not supported error", () => { + expect(() => service.popOutBrowserExtension()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + + describe("closeBrowserExtensionPopout", () => { + it("throws platform not supported error", () => { + expect(() => service.closeBrowserExtensionPopout()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + describe("isWindowVisible", () => { it("returns the window visibility", async () => { isWindowVisibleMock.mockReturnValue(true); diff --git a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts index 72df9336ea2..5cb3803930d 100644 --- a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts +++ b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts @@ -27,6 +27,14 @@ export class DesktopLockComponentService implements LockComponentService { return null; } + popOutBrowserExtension(): Promise { + throw new Error("Method not supported on this platform."); + } + + closeBrowserExtensionPopout(): void { + throw new Error("Method not supported on this platform."); + } + async isWindowVisible(): Promise { return ipc.platform.isWindowVisible(); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8850cbe5a3f..81e3a94ff4d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -393,6 +393,64 @@ "authenticatorKeyTotp": { "message": "Authenticator key (TOTP)" }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "message": "Autofill options" + }, + "websiteUri": { + "message": "Website (URI)" + }, + "websiteUriCount": { + "message": "Website (URI) $COUNT$", + "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "websiteAdded": { + "message": "Website added" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "owner": { + "message": "Owner" + }, + "addField": { + "message": "Add field" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "add": { + "message": "Add" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, "folder": { "message": "Folder" }, @@ -418,6 +476,9 @@ "message": "Linked", "description": "This describes a field that is 'linked' (related) to another field." }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "linkedValue": { "message": "Linked value", "description": "This describes a value that is 'linked' (related) to another value." @@ -1915,6 +1976,43 @@ } } }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, + "copyTOTP": { + "message": "Copy Authenticator key (TOTP)" + }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "totpHelperWithCapture": { + "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + }, + "premium": { + "message": "Premium", + "description": "Premium membership" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2084,6 +2182,15 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact information" + }, "allSends": { "message": "All Sends", "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2518,6 +2625,9 @@ "generateEmail": { "message": "Generate email" }, + "usernameGenerator": { + "message": "Username generator" + }, "spinboxBoundariesHint": { "message": "Value must be between $MIN$ and $MAX$.", "description": "Explains spin box minimum and maximum values to the user", @@ -3212,6 +3322,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "launchDuo": { "message": "Launch Duo in Browser" }, @@ -3436,6 +3549,17 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "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'" + }, "biometricsStatusHelptextUnlockNeeded": { "message": "Biometric unlock is unavailable because PIN or password unlock is required first." }, @@ -3514,6 +3638,27 @@ "setupTwoStepLogin": { "message": "Set up two-step login" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "loginCredentials": { + "message": "Login credentials" + }, + "additionalOptions": { + "message": "Additional options" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "upload": { + "message": "Upload" + }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 3ddac01b47c..94da0f28880 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -27,7 +27,7 @@ "message": "Nota segura" }, "typeSshKey": { - "message": "SSH key" + "message": "Clave SSH" }, "folders": { "message": "Carpetas" @@ -205,19 +205,19 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "Se generó una nueva clave SSH" }, "sshKeyWrongPassword": { "message": "La contraseña introducida es incorrecta." }, "importSshKey": { - "message": "Import" + "message": "Importar" }, "confirmSshKeyPassword": { "message": "Confirmar contraseña" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Introduce la contraseña para la clave SSH." }, "enterSshKeyPassword": { "message": "Introducir la contraseña" @@ -250,17 +250,17 @@ "message": "Error" }, "decryptionError": { - "message": "Decryption error" + "message": "Error de descifrado" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden no pudo descifrar el/los elemento(s) de la caja fuerte listados a continuación." }, "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": "para evitar pérdida de datos adicionales.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "january": { @@ -475,10 +475,10 @@ "message": "Copiar contraseña" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "Regenerar clave SSH" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "Copiar clave privada SSH" }, "copyPassphrase": { "message": "Copy passphrase", @@ -516,7 +516,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Incluir letras mayúsculas", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -524,7 +524,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Incluir letras minúsculas", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -540,7 +540,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Incluir caracteres especiales", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -571,7 +571,7 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Evitar caracteres ambiguos", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -646,16 +646,16 @@ "message": "Identificarse" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Iniciar sesión en Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Introduce el código enviado a tu correo electrónico" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Introduce el código de tu aplicación de autenticación" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "Pulsa tu YubiKey para autenticarte" }, "logInWithPasskey": { "message": "Log in with passkey" @@ -722,7 +722,7 @@ "message": "Unirse a organización" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Unirse a $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -825,7 +825,7 @@ "message": "La autenticación fue cancelada o tardó demasiado. Por favor, inténtalo de nuevo." }, "openInNewTab": { - "message": "Open in new tab" + "message": "Abrir en nueva pestaña" }, "invalidVerificationCode": { "message": "Código de verificación incorrecto" @@ -843,7 +843,7 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "No volver a preguntar en este dispositivo durante 30 días" }, "selectAnotherMethod": { "message": "Select another method", @@ -883,10 +883,10 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "Verifica tu Identidad" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "No reconocemos este dispositivo. Introduce el código enviado a tu correo electrónico para verificar tu identidad." }, "continueLoggingIn": { "message": "Continue logging in" @@ -974,7 +974,7 @@ "message": "No" }, "location": { - "message": "Location" + "message": "Ubicación" }, "overwritePassword": { "message": "Sobreescribir contraseña" @@ -1093,7 +1093,7 @@ "message": "Tu caja fuerte está bloqueada. Verifica tu contraseña maestra para continuar." }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Tu cuenta está bloqueada" }, "or": { "message": "o" @@ -1340,7 +1340,7 @@ "description": "Copy credit card number" }, "copyEmail": { - "message": "Copy email" + "message": "Copiar correo electrónico" }, "copySecurityCode": { "message": "Copiar código de seguridad", @@ -1426,13 +1426,13 @@ "message": "No hay contraseñas que listar." }, "clearHistory": { - "message": "Clear history" + "message": "Limpiar historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Nada que mostrar" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "No has generado nada recientemente" }, "undo": { "message": "Deshacer" @@ -1773,7 +1773,7 @@ "message": "Solicitar contraseña o PIN al iniciar la aplicación" }, "requirePasswordWithoutPinOnStart": { - "message": "Require password on app start" + "message": "Solicitar contraseña al iniciar la aplicación" }, "recommendedForSecurity": { "message": "Recomendado por seguridad." @@ -1791,7 +1791,7 @@ "message": "El borrado de su cuenta es una operación permanente. No se puede deshacer." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "No se puede eliminar la cuenta" }, "cannotDeleteAccountDesc": { "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." @@ -1907,7 +1907,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "de $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2073,7 +2073,7 @@ "message": "Debido a una política de organización, tiene restringido el guardar elementos a su caja fuerte personal. Cambie la configuración de propietario a organización y elija entre las colecciones disponibles." }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "Tu nueva contraseña no puede ser la misma que tu contraseña actual." }, "hintEqualsPassword": { "message": "La pista para la contraseña no puede ser igual que la contraseña." @@ -2248,7 +2248,7 @@ "message": "Autenticar WebAuthn" }, "readSecurityKey": { - "message": "Read security key" + "message": "Leer clave de seguridad" }, "awaitingSecurityKeyInteraction": { "message": "Awaiting security key interaction..." @@ -2494,7 +2494,7 @@ "message": "Bloqueado" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Tu caja fuerte está bloqueada" }, "unlocked": { "message": "Desbloqueado" @@ -2516,7 +2516,7 @@ "message": "Generar nombre de usuario" }, "generateEmail": { - "message": "Generate email" + "message": "Generar correo electrónico" }, "spinboxBoundariesHint": { "message": "Value must be between $MIN$ and $MAX$.", @@ -2569,7 +2569,7 @@ "message": "Utiliza la bandeja de entrada Catch-All configurada por tu dominio." }, "useThisEmail": { - "message": "Use this email" + "message": "Usar este correo electrónico" }, "random": { "message": "Aleatorio" @@ -2782,7 +2782,7 @@ "message": "Inicio de sesión iniciado" }, "logInRequestSent": { - "message": "Request sent" + "message": "Solicitud enviada" }, "notificationSentDevice": { "message": "Se ha enviado una notificación a tu dispositivo." @@ -2800,7 +2800,7 @@ "message": "Make sure the Fingerprint phrase matches the one below before approving." }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "¿Necesitas otra opción?" }, "fingerprintMatchInfo": { "message": "Por favor, asegúrese de que su caja fuerte está desbloqueada y la frase de huella dactilar coincide con el otro dispositivo." @@ -2809,7 +2809,7 @@ "message": "Frase de huella dactilar" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Serás notificado una vez que la solicitud sea aprobada" }, "needAnotherOption": { "message": "Iniciar sesión con el dispositivo debe estar habilitado en los ajustes de la aplicación móvil Bitwarden. ¿Necesitas otra opción?" @@ -2849,10 +2849,10 @@ "message": "Hora" }, "confirmAccess": { - "message": "Confirm access" + "message": "Confirmar acceso" }, "denyAccess": { - "message": "Deny access" + "message": "Denegar acceso" }, "logInConfirmedForEmailOnDevice": { "message": "Inicio de sesión confirmado para $EMAIL$ en $DEVICE$", @@ -2889,7 +2889,7 @@ "message": "Esta solicitud ya no es válida." }, "confirmAccessAttempt": { - "message": "Confirm access attempt for $EMAIL$", + "message": "Confirmar intento de acceso para $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -2937,10 +2937,10 @@ "message": "Contraseña débil encontrada en una filtración de datos. Utilice una contraseña única para proteger su cuenta. ¿Está seguro de que desea utilizar una contraseña comprometida?" }, "useThisPassword": { - "message": "Use this password" + "message": "Usar esta contraseña" }, "useThisUsername": { - "message": "Use this username" + "message": "Usar este nombre de usuario" }, "checkForBreaches": { "message": "Comprobar filtración de datos conocidos para esta contraseña" @@ -2952,7 +2952,7 @@ "message": "Importante:" }, "accessing": { - "message": "Accessing" + "message": "Accediendo" }, "accessTokenUnableToBeDecrypted": { "message": "Se ha cerrado la sesión porque tu token de acceso no pudo ser descifrado. Por favor, inicia sesión de nuevo para resolver este problema." @@ -2985,10 +2985,10 @@ "message": "Se requiere aprobación del dispositivo. Selecciona una opción de aprobación a continuación:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Se requiere la aprobación del dispositivo" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Selecciona una opción de aprobación abajo" }, "rememberThisDevice": { "message": "Recordar este dispositivo" @@ -3461,13 +3461,13 @@ "message": "Biometric unlock is currently unavailable for an unknown reason." }, "authorize": { - "message": "Authorize" + "message": "Autorizar" }, "deny": { - "message": "Deny" + "message": "Denegar" }, "sshkeyApprovalTitle": { - "message": "Confirm SSH key usage" + "message": "Confirmar uso de clave SSH" }, "agentForwardingWarningTitle": { "message": "Warning: Agent Forwarding" @@ -3476,7 +3476,7 @@ "message": "This request comes from a remote device that you are logged into" }, "sshkeyApprovalMessageInfix": { - "message": "is requesting access to" + "message": "está solicitando acceso a" }, "sshkeyApprovalMessageSuffix": { "message": "in order to" @@ -3491,25 +3491,25 @@ "message": "sign a git commit" }, "unknownApplication": { - "message": "An application" + "message": "Una aplicación" }, "invalidSshKey": { - "message": "The SSH key is invalid" + "message": "La clave SSH no es válida" }, "sshKeyTypeUnsupported": { - "message": "The SSH key type is not supported" + "message": "El tipo de clave SSH no está soportado" }, "importSshKeyFromClipboard": { - "message": "Import key from clipboard" + "message": "Importar clave del portapapeles" }, "sshKeyImported": { - "message": "SSH key imported successfully" + "message": "Clave SSH importada correctamente" }, "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." }, "importantNotice": { - "message": "Important notice" + "message": "Aviso importante" }, "setupTwoStepLogin": { "message": "Set up two-step login" @@ -3521,10 +3521,10 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Recuérdame más tarde" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "¿Tienes acceso fiable a tu correo electrónico, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -3533,13 +3533,13 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "No, no lo tengo" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Sí, puedo acceder a mi correo electrónico de forma fiable" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Activar inicio de sesión en dos pasos" }, "changeAcctEmail": { "message": "Change account email" @@ -3554,10 +3554,10 @@ "message": "Confirm window still visible" }, "confirmWindowStillVisibleContent": { - "message": "Please confirm that the window is still visible." + "message": "Por favor, confirma que la ventana sigue siendo visible." }, "updateBrowserOrDisableFingerprintDialogTitle": { - "message": "Extension update required" + "message": "Actualización de la extensión requerida" }, "updateBrowserOrDisableFingerprintDialogMessage": { "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." @@ -3566,6 +3566,6 @@ "message": "Change at-risk password" }, "move": { - "message": "Move" + "message": "Mover" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 767a1be15ef..0234608f7a3 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2849,7 +2849,7 @@ "message": "Aika" }, "confirmAccess": { - "message": "Confirm access" + "message": "Myönnä käyttöoikeus" }, "denyAccess": { "message": "Deny access" @@ -3503,7 +3503,7 @@ "message": "Tuo avain leikepöydältä" }, "sshKeyImported": { - "message": "SSH key imported successfully" + "message": "SSH-avain on tuotu" }, "fileSavedToDevice": { "message": "Tiedosto tallennettiin laitteelle. Hallitse sitä laitteesi latauksista." @@ -3524,7 +3524,7 @@ "message": "Muistuta myöhemmin" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Onko sinulla luotettava pääsy sähköpostiisi, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -3533,10 +3533,10 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Ei ole" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Kyllä, voin käyttää sähköpostiani luotettavasti" }, "turnOnTwoStepLogin": { "message": "Ota kaksivaiheinen kirjautuminen käyttöön" @@ -3566,6 +3566,6 @@ "message": "Vaihda vaarantunut salasana" }, "move": { - "message": "Move" + "message": "Siirrä" } } diff --git a/apps/desktop/src/scss/base.scss b/apps/desktop/src/scss/base.scss index 22eb3df0d17..494e91529ee 100644 --- a/apps/desktop/src/scss/base.scss +++ b/apps/desktop/src/scss/base.scss @@ -147,3 +147,8 @@ div:not(.modal)::-webkit-scrollbar-thumb, .mx-auto { margin-left: auto !important; } + +.vault-v2 button:not([bitbutton]):not([biticonbutton]) i.bwi, +a i.bwi { + margin-right: 0.25rem; +} diff --git a/apps/desktop/src/scss/vault.scss b/apps/desktop/src/scss/vault.scss index f7403ad62d2..88216a2b926 100644 --- a/apps/desktop/src/scss/vault.scss +++ b/apps/desktop/src/scss/vault.scss @@ -162,3 +162,7 @@ app-root { } } } + +.vault-v2 > .details { + flex-direction: column-reverse; +} diff --git a/apps/desktop/src/services/desktop-cipher-form-generator.service.ts b/apps/desktop/src/services/desktop-cipher-form-generator.service.ts new file mode 100644 index 00000000000..8a33f4ced0a --- /dev/null +++ b/apps/desktop/src/services/desktop-cipher-form-generator.service.ts @@ -0,0 +1,32 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; +import { CipherFormGenerationService } from "@bitwarden/vault"; + +import { CredentialGeneratorDialogComponent } from "../vault/app/vault/credential-generator-dialog.component"; + +@Injectable() +export class DesktopCredentialGenerationService implements CipherFormGenerationService { + private dialogService = inject(DialogService); + + async generatePassword(): Promise { + return await this.generateCredential("password"); + } + + async generateUsername(uri: string): Promise { + return await this.generateCredential("username", uri); + } + + async generateCredential(type: "password" | "username", uri?: string): Promise { + const dialogRef = CredentialGeneratorDialogComponent.open(this.dialogService, { type, uri }); + + const result = await firstValueFrom(dialogRef.closed); + + if (!result || result.action === "canceled" || !result.generatedValue) { + return ""; + } + + return result.generatedValue; + } +} diff --git a/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts new file mode 100644 index 00000000000..3b33116ea5a --- /dev/null +++ b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts @@ -0,0 +1,30 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service"; + +describe("DesktopPremiumUpgradePromptService", () => { + let service: DesktopPremiumUpgradePromptService; + let messager: MockProxy; + + beforeEach(async () => { + messager = mock(); + await TestBed.configureTestingModule({ + providers: [ + DesktopPremiumUpgradePromptService, + { provide: MessagingService, useValue: messager }, + ], + }).compileComponents(); + + service = TestBed.inject(DesktopPremiumUpgradePromptService); + }); + + describe("promptForPremium", () => { + it("navigates to the premium update screen", async () => { + await service.promptForPremium(); + expect(messager.send).toHaveBeenCalledWith("openPremium"); + }); + }); +}); diff --git a/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts new file mode 100644 index 00000000000..f2375ecfebb --- /dev/null +++ b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts @@ -0,0 +1,15 @@ +import { inject } from "@angular/core"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; + +/** + * This class handles the premium upgrade process for the desktop. + */ +export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService { + private messagingService = inject(MessagingService); + + async promptForPremium() { + this.messagingService.send("openPremium"); + } +} diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html index 47232dff66d..31f47d824d6 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html @@ -3,6 +3,7 @@ @@ -27,7 +28,6 @@ (click)="applyCredentials()" appA11yTitle="{{ buttonLabel }}" bitButton - bitDialogClose [disabled]="!(buttonLabel && credentialValue)" > {{ buttonLabel }} diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index eda35a8c76d..2858d7330e5 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ItemModule, LinkModule, + DialogRef, } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, @@ -19,10 +20,22 @@ import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; type CredentialGeneratorParams = { - onCredentialGenerated: (value?: string) => void; + /** @deprecated Prefer use of dialogRef.closed to retreive the generated value */ + onCredentialGenerated?: (value?: string) => void; type: "password" | "username"; + uri?: string; }; +export interface CredentialGeneratorDialogResult { + action: CredentialGeneratorDialogAction; + generatedValue?: string; +} + +export enum CredentialGeneratorDialogAction { + Selected = "selected", + Canceled = "canceled", +} + @Component({ standalone: true, selector: "credential-generator-dialog", @@ -45,6 +58,7 @@ export class CredentialGeneratorDialogComponent { constructor( @Inject(DIALOG_DATA) protected data: CredentialGeneratorParams, private dialogService: DialogService, + private dialogRef: DialogRef, private i18nService: I18nService, ) {} @@ -59,11 +73,15 @@ export class CredentialGeneratorDialogComponent { }; applyCredentials = () => { - this.data.onCredentialGenerated(this.credentialValue); + this.data.onCredentialGenerated?.(this.credentialValue); + this.dialogRef.close({ + action: CredentialGeneratorDialogAction.Selected, + generatedValue: this.credentialValue, + }); }; clearCredentials = () => { - this.data.onCredentialGenerated(); + this.data.onCredentialGenerated?.(); }; onCredentialGenerated = (value: string) => { @@ -75,9 +93,12 @@ export class CredentialGeneratorDialogComponent { this.dialogService.open(CredentialGeneratorHistoryDialogComponent); }; - static open = (dialogService: DialogService, data: CredentialGeneratorParams) => { - dialogService.open(CredentialGeneratorDialogComponent, { - data, - }); - }; + static open(dialogService: DialogService, data: CredentialGeneratorParams) { + return dialogService.open( + CredentialGeneratorDialogComponent, + { + data, + }, + ); + } } diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html new file mode 100644 index 00000000000..6915555c08b --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.html @@ -0,0 +1,64 @@ + diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts new file mode 100644 index 00000000000..639d1557ecd --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -0,0 +1,159 @@ +import { CommonModule } from "@angular/common"; +import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core"; +import { Observable, firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { ButtonComponent, ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-vault-item-footer", + templateUrl: "item-footer.component.html", + standalone: true, + imports: [ButtonModule, CommonModule, JslibModule], +}) +export class ItemFooterComponent implements OnInit { + @Input({ required: true }) cipher: CipherView = new CipherView(); + @Input() collectionId: string | null = null; + @Input({ required: true }) action: string = "view"; + @Input() isSubmitting: boolean = false; + @Output() onEdit = new EventEmitter(); + @Output() onClone = new EventEmitter(); + @Output() onDelete = new EventEmitter(); + @Output() onRestore = new EventEmitter(); + @Output() onCancel = new EventEmitter(); + @ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null; + + canDeleteCipher$: Observable = new Observable(); + activeUserId: UserId | null = null; + + private passwordReprompted = false; + + constructor( + protected cipherService: CipherService, + protected dialogService: DialogService, + protected passwordRepromptService: PasswordRepromptService, + protected cipherAuthorizationService: CipherAuthorizationService, + protected accountService: AccountService, + protected toastService: ToastService, + protected i18nService: I18nService, + protected logService: LogService, + ) {} + + async ngOnInit() { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ + this.collectionId as CollectionId, + ]); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + } + + async clone() { + if (this.cipher.login?.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "passkeyNotCopied" }, + content: { key: "passkeyNotCopiedAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + + if (await this.promptPassword()) { + this.onClone.emit(this.cipher); + return true; + } + + return false; + } + + protected edit() { + this.onEdit.emit(this.cipher); + } + + cancel() { + this.onCancel.emit(this.cipher); + } + + async delete(): Promise { + if (!(await this.promptPassword())) { + return false; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipher(activeUserId); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); + this.onDelete.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.restoreCipher(activeUserId); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("restoredItem"), + }); + this.onRestore.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + protected deleteCipher(userId: UserId) { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id, userId) + : this.cipherService.softDeleteWithServer(this.cipher.id, userId); + } + + protected restoreCipher(userId: UserId) { + return this.cipherService.restoreWithServer(this.cipher.id, userId); + } + + protected async promptPassword() { + if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) { + return true; + } + + return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt()); + } +} diff --git a/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.module.ts b/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.module.ts index fb6706bef1c..8729996c835 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.module.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.module.ts @@ -1,5 +1,5 @@ +import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { BrowserModule } from "@angular/platform-browser"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/vault/abstractions/deprecated-vault-filter.service"; @@ -13,7 +13,7 @@ import { TypeFilterComponent } from "./filters/type-filter.component"; import { VaultFilterComponent } from "./vault-filter.component"; @NgModule({ - imports: [BrowserModule, JslibModule], + imports: [CommonModule, JslibModule], declarations: [ VaultFilterComponent, CollectionFilterComponent, diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html new file mode 100644 index 00000000000..ff35e00fb0f --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -0,0 +1,92 @@ +
+ +
+ +
+ +
+ +
+
+
+ +

{{ "noItemsInList" | i18n }}

+ +
+ +
+
+ + + + + + + + + + diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts new file mode 100644 index 00000000000..31d4098d2b2 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -0,0 +1,42 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { distinctUntilChanged } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { MenuModule } from "@bitwarden/components"; + +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; + +@Component({ + selector: "app-vault-items-v2", + templateUrl: "vault-items-v2.component.html", + standalone: true, + imports: [MenuModule, CommonModule, JslibModule, ScrollingModule], +}) +export class VaultItemsV2Component extends BaseVaultItemsComponent { + constructor( + searchService: SearchService, + private readonly searchBarService: SearchBarService, + cipherService: CipherService, + accountService: AccountService, + ) { + super(searchService, cipherService, accountService); + + this.searchBarService.searchText$ + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe((searchText) => { + this.searchText = searchText!; + }); + } + + trackByFn(index: number, c: CipherView): string { + return c.id; + } +} diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html new file mode 100644 index 00000000000..12f52502984 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -0,0 +1,80 @@ +
+ + +
+ +
+
+
+ + + + + + + +
+
+
+
+ +
+ + +
+
+ diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts new file mode 100644 index 00000000000..7e799899418 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -0,0 +1,785 @@ +import { CommonModule } from "@angular/common"; +import { + ChangeDetectorRef, + Component, + NgZone, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; +import { filter, map, take } from "rxjs/operators"; + +import { CollectionView } from "@bitwarden/admin-console/common"; +import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; +import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EventType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + BadgeModule, + ButtonModule, + DialogService, + ItemModule, + ToastService, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { + AttachmentDialogResult, + AttachmentsV2Component, + ChangeLoginPasswordService, + CipherFormConfig, + CipherFormConfigService, + CipherFormGenerationService, + CipherFormMode, + CipherFormModule, + CipherViewComponent, + DecryptionFailureDialogComponent, + DefaultChangeLoginPasswordService, + DefaultCipherFormConfigService, + PasswordRepromptService, +} from "@bitwarden/vault"; + +import { NavComponent } from "../../../app/layout/nav.component"; +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; +import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service"; +import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; +import { invokeMenu, RendererMenuItem } from "../../../utils"; + +import { FolderAddEditComponent } from "./folder-add-edit.component"; +import { ItemFooterComponent } from "./item-footer.component"; +import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; +import { VaultFilterModule } from "./vault-filter/vault-filter.module"; +import { VaultItemsV2Component } from "./vault-items-v2.component"; + +const BroadcasterSubscriptionId = "VaultComponent"; + +@Component({ + selector: "app-vault", + templateUrl: "vault-v2.component.html", + standalone: true, + imports: [ + BadgeModule, + CommonModule, + CipherFormModule, + CipherViewComponent, + ItemFooterComponent, + I18nPipe, + ItemModule, + ButtonModule, + NavComponent, + VaultFilterModule, + VaultItemsV2Component, + ], + providers: [ + { + provide: CipherFormConfigService, + useClass: DefaultCipherFormConfigService, + }, + { + provide: ChangeLoginPasswordService, + useClass: DefaultChangeLoginPasswordService, + }, + { + provide: ViewPasswordHistoryService, + useClass: VaultViewPasswordHistoryService, + }, + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + { provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService }, + ], +}) +export class VaultV2Component implements OnInit, OnDestroy { + @ViewChild(VaultItemsV2Component, { static: true }) + vaultItemsComponent: VaultItemsV2Component | null = null; + @ViewChild(VaultFilterComponent, { static: true }) + vaultFilterComponent: VaultFilterComponent | null = null; + @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) + folderAddEditModalRef: ViewContainerRef | null = null; + + action: CipherFormMode | "view" | null = null; + cipherId: string | null = null; + favorites = false; + type: CipherType | null = null; + folderId: string | null = null; + collectionId: string | null = null; + organizationId: string | null = null; + myVaultOnly = false; + addType: CipherType | undefined = undefined; + addOrganizationId: string | null = null; + addCollectionIds: string[] | null = null; + showingModal = false; + deleted = false; + userHasPremiumAccess = false; + activeFilter: VaultFilter = new VaultFilter(); + activeUserId: UserId | null = null; + cipherRepromptId: string | null = null; + cipher: CipherView | null = new CipherView(); + collections: CollectionView[] | null = null; + config: CipherFormConfig | null = null; + + protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); + + private modal: ModalRef | null = null; + private componentIsDestroyed$ = new Subject(); + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private modalService: ModalService, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private syncService: SyncService, + private messagingService: MessagingService, + private platformUtilsService: PlatformUtilsService, + private eventCollectionService: EventCollectionService, + private totpService: TotpService, + private passwordRepromptService: PasswordRepromptService, + private searchBarService: SearchBarService, + private apiService: ApiService, + private dialogService: DialogService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + private accountService: AccountService, + private cipherService: CipherService, + private formConfigService: CipherFormConfigService, + private premiumUpgradePromptService: PremiumUpgradePromptService, + ) {} + + async ngOnInit() { + this.accountService.activeAccount$ + .pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((canAccessPremium: boolean) => { + this.userHasPremiumAccess = canAccessPremium; + }); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone + .run(async () => { + let detectChanges = true; + try { + switch (message.command) { + case "newLogin": + await this.addCipher(CipherType.Login).catch(() => {}); + break; + case "newCard": + await this.addCipher(CipherType.Card).catch(() => {}); + break; + case "newIdentity": + await this.addCipher(CipherType.Identity).catch(() => {}); + break; + case "newSecureNote": + await this.addCipher(CipherType.SecureNote).catch(() => {}); + break; + case "focusSearch": + (document.querySelector("#search") as HTMLInputElement)?.select(); + detectChanges = false; + break; + case "syncCompleted": + if (this.vaultItemsComponent) { + await this.vaultItemsComponent + .reload(this.activeFilter.buildFilter()) + .catch(() => {}); + } + if (this.vaultFilterComponent) { + await this.vaultFilterComponent + .reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + await this.vaultFilterComponent.reloadOrganizations().catch(() => {}); + } + break; + case "modalShown": + this.showingModal = true; + break; + case "modalClosed": + this.showingModal = false; + break; + case "copyUsername": { + if (this.cipher?.login?.username) { + this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username"); + } + break; + } + case "copyPassword": { + if (this.cipher?.login?.password && this.cipher.viewPassword) { + this.copyValue(this.cipher, this.cipher.login.password, "password", "Password"); + await this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id) + .catch(() => {}); + } + break; + } + case "copyTotp": { + if ( + this.cipher?.login?.hasTotp && + (this.cipher.organizationUseTotp || this.userHasPremiumAccess) + ) { + const value = await firstValueFrom( + this.totpService.getCode$(this.cipher.login.totp), + ).catch(() => null); + if (value) { + this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP"); + } + } + break; + } + default: + detectChanges = false; + break; + } + } catch { + // Ignore errors + } + if (detectChanges) { + this.changeDetectorRef.detectChanges(); + } + }) + .catch(() => {}); + }); + + if (!this.syncService.syncInProgress) { + await this.load().catch(() => {}); + } + + this.searchBarService.setEnabled(true); + this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); + + const authRequest = await this.apiService.getLastAuthRequest().catch(() => null); + if (authRequest != null) { + this.messagingService.send("openLoginApproval", { + notificationId: authRequest.id, + }); + } + + this.activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ).catch(() => null); + + if (this.activeUserId) { + this.cipherService + .failedToDecryptCiphers$(this.activeUserId) + .pipe( + map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); + } + } + + ngOnDestroy() { + this.searchBarService.setEnabled(false); + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.componentIsDestroyed$.next(true); + this.componentIsDestroyed$.complete(); + } + + async load() { + const params = await firstValueFrom(this.route.queryParams).catch(); + if (params.cipherId) { + const cipherView = new CipherView(); + cipherView.id = params.cipherId; + if (params.action === "clone") { + await this.cloneCipher(cipherView).catch(() => {}); + } else if (params.action === "edit") { + await this.editCipher(cipherView).catch(() => {}); + } else { + await this.viewCipher(cipherView).catch(() => {}); + } + } else if (params.action === "add") { + this.addType = Number(params.addType); + await this.addCipher(this.addType).catch(() => {}); + } + + this.activeFilter = new VaultFilter({ + status: params.deleted ? "trash" : params.favorites ? "favorites" : "all", + cipherType: + params.action === "add" || params.type == null + ? undefined + : (parseInt(params.type) as CipherType), + selectedFolderId: params.folderId, + selectedCollectionId: params.selectedCollectionId, + selectedOrganizationId: params.selectedOrganizationId, + myVaultOnly: params.myVaultOnly ?? false, + }); + if (this.vaultItemsComponent) { + await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {}); + } + } + + async viewCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "view")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + this.collections = + this.vaultFilterComponent?.collections.fullList.filter((c) => + cipher.collectionIds.includes(c.id), + ) ?? null; + this.action = "view"; + await this.go().catch(() => {}); + } + + async openAttachmentsDialog() { + if (!this.userHasPremiumAccess) { + await this.premiumUpgradePromptService.promptForPremium(); + return; + } + const dialogRef = AttachmentsV2Component.open(this.dialogService, { + cipherId: this.cipherId as CipherId, + }); + const result = await firstValueFrom(dialogRef.closed).catch(() => null); + if ( + result?.action === AttachmentDialogResult.Removed || + result?.action === AttachmentDialogResult.Uploaded + ) { + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + } + + viewCipherMenu(cipher: CipherView) { + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("view"), + click: () => { + this.functionWithChangeDetection(() => { + this.viewCipher(cipher).catch(() => {}); + }); + }, + }, + ]; + + if (cipher.decryptionFailure) { + invokeMenu(menu); + return; + } + + if (!cipher.isDeleted) { + menu.push({ + label: this.i18nService.t("edit"), + click: () => { + this.functionWithChangeDetection(() => { + this.editCipher(cipher).catch(() => {}); + }); + }, + }); + if (!cipher.organizationId) { + menu.push({ + label: this.i18nService.t("clone"), + click: () => { + this.functionWithChangeDetection(() => { + this.cloneCipher(cipher).catch(() => {}); + }); + }, + }); + } + } + + switch (cipher.type) { + case CipherType.Login: + if ( + cipher.login.canLaunch || + cipher.login.username != null || + cipher.login.password != null + ) { + menu.push({ type: "separator" }); + } + if (cipher.login.canLaunch) { + menu.push({ + label: this.i18nService.t("launch"), + click: () => this.platformUtilsService.launchUri(cipher.login.launchUri), + }); + } + if (cipher.login.username != null) { + menu.push({ + label: this.i18nService.t("copyUsername"), + click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"), + }); + } + if (cipher.login.password != null && cipher.viewPassword) { + menu.push({ + label: this.i18nService.t("copyPassword"), + click: () => { + this.copyValue(cipher, cipher.login.password, "password", "Password"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, cipher.id) + .catch(() => {}); + }, + }); + } + if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) { + menu.push({ + label: this.i18nService.t("copyVerificationCodeTotp"), + click: async () => { + const value = await firstValueFrom( + this.totpService.getCode$(cipher.login.totp), + ).catch(() => null); + if (value) { + this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP"); + } + }, + }); + } + break; + case CipherType.Card: + if (cipher.card.number != null || cipher.card.code != null) { + menu.push({ type: "separator" }); + } + if (cipher.card.number != null) { + menu.push({ + label: this.i18nService.t("copyNumber"), + click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"), + }); + } + if (cipher.card.code != null) { + menu.push({ + label: this.i18nService.t("copySecurityCode"), + click: () => { + this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedCardCode, cipher.id) + .catch(() => {}); + }, + }); + } + break; + default: + break; + } + invokeMenu(menu); + } + + async shouldReprompt(cipher: CipherView, action: "edit" | "clone" | "view"): Promise { + return !(await this.canNavigateAway(action, cipher)) || !(await this.passwordReprompt(cipher)); + } + + async buildFormConfig(action: CipherFormMode) { + this.config = await this.formConfigService + .buildConfig(action, this.cipherId as CipherId, this.addType) + .catch(() => null); + } + + async editCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "edit")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + await this.buildFormConfig("edit"); + this.action = "edit"; + await this.go().catch(() => {}); + } + + async cloneCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "clone")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + await this.buildFormConfig("clone"); + this.action = "clone"; + await this.go().catch(() => {}); + } + + async addCipher(type: CipherType) { + this.addType = type || this.activeFilter.cipherType; + this.cipherId = null; + await this.buildFormConfig("add"); + this.action = "add"; + this.prefillCipherFromFilter(); + await this.go().catch(() => {}); + } + + addCipherOptions() { + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("typeLogin"), + click: () => this.addCipherWithChangeDetection(CipherType.Login), + }, + { + label: this.i18nService.t("typeCard"), + click: () => this.addCipherWithChangeDetection(CipherType.Card), + }, + { + label: this.i18nService.t("typeIdentity"), + click: () => this.addCipherWithChangeDetection(CipherType.Identity), + }, + { + label: this.i18nService.t("typeSecureNote"), + click: () => this.addCipherWithChangeDetection(CipherType.SecureNote), + }, + ]; + invokeMenu(menu); + } + + async savedCipher(cipher: CipherView) { + this.cipherId = null; + this.action = "view"; + await this.vaultItemsComponent?.refresh().catch(() => {}); + this.cipherId = cipher.id; + this.cipher = cipher; + if (this.activeUserId) { + await this.cipherService.clearCache(this.activeUserId).catch(() => {}); + } + await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {}); + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async deleteCipher() { + this.cipherId = null; + this.cipher = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async restoreCipher() { + this.cipherId = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async cancelCipher(cipher: CipherView) { + this.cipherId = cipher.id; + this.cipher = cipher; + this.action = this.cipherId != null ? "view" : null; + await this.go().catch(() => {}); + } + + async applyVaultFilter(vaultFilter: VaultFilter) { + this.searchBarService.setPlaceholderText( + this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), + ); + this.activeFilter = vaultFilter; + await this.vaultItemsComponent + ?.reload(this.activeFilter.buildFilter(), vaultFilter.status === "trash") + .catch(() => {}); + await this.go().catch(() => {}); + } + + private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { + if (vaultFilter.status === "favorites") { + return "searchFavorites"; + } + if (vaultFilter.status === "trash") { + return "searchTrash"; + } + if (vaultFilter.cipherType != null) { + return "searchType"; + } + if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") { + return "searchFolder"; + } + if (vaultFilter.selectedCollectionId != null) { + return "searchCollection"; + } + if (vaultFilter.selectedOrganizationId != null) { + return "searchOrganization"; + } + if (vaultFilter.myVaultOnly) { + return "searchMyVault"; + } + return "searchVault"; + } + + async addFolder() { + this.messagingService.send("newFolder"); + } + + async editFolder(folderId: string) { + if (this.modal != null) { + this.modal.close(); + } + if (this.folderAddEditModalRef == null) { + return; + } + const [modal, childComponent] = await this.modalService + .openViewRef( + FolderAddEditComponent, + this.folderAddEditModalRef, + (comp) => (comp.folderId = folderId), + ) + .catch(() => [null, null] as any); + this.modal = modal; + if (childComponent) { + childComponent.onSavedFolder.subscribe(async (folder: FolderView) => { + this.modal?.close(); + await this.vaultFilterComponent + ?.reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + }); + childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => { + this.modal?.close(); + await this.vaultFilterComponent + ?.reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + }); + } + if (this.modal) { + this.modal.onClosed.pipe(takeUntilDestroyed()).subscribe(() => { + this.modal = null; + }); + } + } + + private dirtyInput(): boolean { + return ( + (this.action === "add" || this.action === "edit" || this.action === "clone") && + document.querySelectorAll("vault-cipher-form .ng-dirty").length > 0 + ); + } + + private async wantsToSaveChanges(): Promise { + const confirmed = await this.dialogService + .openSimpleDialog({ + title: { key: "unsavedChangesTitle" }, + content: { key: "unsavedChangesConfirmation" }, + type: "warning", + }) + .catch(() => false); + return !confirmed; + } + + private async go(queryParams: any = null) { + if (queryParams == null) { + queryParams = { + action: this.action, + cipherId: this.cipherId, + favorites: this.favorites ? true : null, + type: this.type, + folderId: this.folderId, + collectionId: this.collectionId, + deleted: this.deleted ? true : null, + organizationId: this.organizationId, + myVaultOnly: this.myVaultOnly, + }; + } + this.router + .navigate([], { + relativeTo: this.route, + queryParams: queryParams, + replaceUrl: true, + }) + .catch(() => {}); + } + + private addCipherWithChangeDetection(type: CipherType) { + this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {})); + } + + private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) { + this.functionWithChangeDetection(() => { + (async () => { + if ( + cipher.reprompt !== CipherRepromptType.None && + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.passwordReprompt(cipher)) + ) { + return; + } + this.platformUtilsService.copyToClipboard(value); + this.toastService.showToast({ + variant: "info", + title: undefined, + message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)), + }); + if (this.action === "view") { + this.messagingService.send("minimizeOnCopy"); + } + })().catch(() => {}); + }); + } + + private functionWithChangeDetection(func: () => void) { + this.ngZone.run(() => { + func(); + this.changeDetectorRef.detectChanges(); + }); + } + + private prefillCipherFromFilter() { + if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) { + const collections = this.vaultFilterComponent.collections.fullList.filter( + (c) => c.id === this.activeFilter.selectedCollectionId, + ); + if (collections.length > 0) { + this.addOrganizationId = collections[0].organizationId; + this.addCollectionIds = [this.activeFilter.selectedCollectionId]; + } + } else if (this.activeFilter.selectedOrganizationId) { + this.addOrganizationId = this.activeFilter.selectedOrganizationId; + } + if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { + this.folderId = this.activeFilter.selectedFolderId; + } + } + + private async canNavigateAway(action: string, cipher?: CipherView) { + if (this.action === action && (!cipher || this.cipherId === cipher.id)) { + return false; + } else if (this.dirtyInput() && (await this.wantsToSaveChanges())) { + return false; + } + return true; + } + + private async passwordReprompt(cipher: CipherView) { + if (cipher.reprompt === CipherRepromptType.None) { + this.cipherRepromptId = null; + return true; + } + if (this.cipherRepromptId === cipher.id) { + return true; + } + const repromptResult = await this.passwordRepromptService.showPasswordPrompt(); + if (repromptResult) { + this.cipherRepromptId = cipher.id; + } + return repromptResult; + } +} diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 97193bf1b1f..8cb54d9a911 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -69,6 +69,8 @@ import { ToastService, } from "@bitwarden/components"; import { + AttachmentDialogResult, + AttachmentsV2Component, CipherFormConfig, CipherFormConfigService, CollectionAssignmentResult, @@ -92,10 +94,6 @@ import { } from "../../../vault/components/vault-item-dialog/vault-item-dialog.component"; import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event"; import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module"; -import { - AttachmentDialogResult, - AttachmentsV2Component, -} from "../../../vault/individual-vault/attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, diff --git a/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts b/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts index 1a83fed37b7..8579c4c1dc8 100644 --- a/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts +++ b/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts @@ -36,7 +36,7 @@ describe("RotateableKeySetService", () => { keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]); keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any); encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any); - encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any); + encryptService.wrapEncapsulationKey.mockResolvedValue(encryptedPublicKey as any); const result = await service.createKeySet(externalKey as any); diff --git a/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts b/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts index 8510aa1c29a..ef78e09e6b9 100644 --- a/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts +++ b/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts @@ -29,7 +29,10 @@ export class RotateableKeySetService { userKey, rawPublicKey, ); - const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey); + const encryptedPublicKey = await this.encryptService.wrapEncapsulationKey( + rawPublicKey, + userKey, + ); return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey); } @@ -62,7 +65,10 @@ export class RotateableKeySetService { if (publicKey == null) { throw new Error("failed to rotate key set: could not decrypt public key"); } - const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); + const newEncryptedPublicKey = await this.encryptService.wrapEncapsulationKey( + publicKey, + newUserKey, + ); const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( newUserKey, publicKey, diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index 67dbee223d1..0a30aa16478 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -92,6 +92,9 @@ describe("AcceptOrganizationInviteService", () => { "orgPublicKey", { encryptedString: "string" } as EncString, ]); + encryptService.wrapDecapsulationKey.mockResolvedValue({ + encryptedString: "string", + } as EncString); encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString); const invite = createOrgInvite({ initOrganization: true }); diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index bcae11c3264..0022da7f3a9 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EmergencyAccessId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; @@ -21,8 +22,6 @@ import { DefaultChangeLoginPasswordService, } from "@bitwarden/vault"; -import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service"; - export interface EmergencyViewDialogParams { /** The cipher being viewed. */ cipher: CipherView; @@ -42,7 +41,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { standalone: true, imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule], providers: [ - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop }, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 59f8dd34c37..e373b0d4dee 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -812,7 +812,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ); const providerKey = await this.keyService.getProviderKey(this.providerId); providerRequest.organizationCreateRequest.key = ( - await this.encryptService.encrypt(orgKey.key, providerKey) + await this.encryptService.wrapSymmetricKey(orgKey, providerKey) ).encryptedString; const orgId = ( await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest) diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts deleted file mode 100644 index 0f9db7b4405..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-abm-enterprise-content", - templateUrl: "abm-enterprise-content.component.html", -}) -export class AbmEnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts deleted file mode 100644 index 7765555f5cc..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-abm-teams-content", - templateUrl: "abm-teams-content.component.html", -}) -export class AbmTeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html deleted file mode 100644 index b5c16911ab0..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Enterprise Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts deleted file mode 100644 index 4a6de8d3003..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-enterprise-content", - templateUrl: "cnet-enterprise-content.component.html", -}) -export class CnetEnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html deleted file mode 100644 index 6e6f545c170..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Premium Account Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Secure your account with advanced two-step login
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts deleted file mode 100644 index 56d8b37af90..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-individual-content", - templateUrl: "cnet-individual-content.component.html", -}) -export class CnetIndividualContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html deleted file mode 100644 index c719c5ac7ce..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Teams Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts deleted file mode 100644 index ff79a0d37cd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-teams-content", - templateUrl: "cnet-teams-content.component.html", -}) -export class CnetTeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/default-content.component.html b/apps/web/src/app/billing/trial-initiation/content/default-content.component.html deleted file mode 100644 index e1839517ff6..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/default-content.component.html +++ /dev/null @@ -1,16 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts deleted file mode 100644 index 7ad40b089d1..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-default-content", - templateUrl: "default-content.component.html", -}) -export class DefaultContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts deleted file mode 100644 index 847b3c3088a..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise-content", - templateUrl: "enterprise-content.component.html", -}) -export class EnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts deleted file mode 100644 index 7b1199eb421..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise1-content", - templateUrl: "enterprise1-content.component.html", -}) -export class Enterprise1ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts deleted file mode 100644 index 08dec6190c7..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise2-content", - templateUrl: "enterprise2-content.component.html", -}) -export class Enterprise2ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html deleted file mode 100644 index d1b33eab3a4..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
- - third party awards - -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts deleted file mode 100644 index c23432b67cf..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-badges", - templateUrl: "logo-badges.component.html", -}) -export class LogoBadgesComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html deleted file mode 100644 index fb4537d2820..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
- - - - - -
-
- “Bitwarden scores points for being fully open-source, secure and audited annually by third-party - cybersecurity firms, giving it a level of transparency that sets it apart from its peers.” -
-
- - CNET Logo - -

Best Password Manager in 2024

-
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts deleted file mode 100644 index af531829d50..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-cnet-5-stars", - templateUrl: "logo-cnet-5-stars.component.html", -}) -export class LogoCnet5StarsComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html deleted file mode 100644 index 4e04cec6da4..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- - CNET Logo - -
-
- "No more excuses; start using Bitwarden today. The identity you save could be your own. The - money definitely will be." -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts deleted file mode 100644 index 4f755f66a86..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-cnet", - templateUrl: "logo-cnet.component.html", -}) -export class LogoCnetComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html deleted file mode 100644 index 0b81e0bd216..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html +++ /dev/null @@ -1,28 +0,0 @@ -
-

- Recommended by industry experts -

-
-
- CNET Logo - WIRED Logo -
-
- New York Times Logo - PC Mag Logo -
-
-
- “Bitwarden is currently CNET's top pick for the best password manager, thanks in part to - its commitment to transparency and its unbeatable free tier.” -
-

Best Password Manager in 2024

-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts deleted file mode 100644 index 9d9c4471820..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-company-testimonial", - templateUrl: "logo-company-testimonial.component.html", -}) -export class LogoCompanyTestimonialComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html deleted file mode 100644 index 34426168324..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- - Forbes Logo - -
-
- “Bitwarden boasts the backing of some of the world's best security experts and an attractive, - easy-to-use interface” -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts deleted file mode 100644 index 818721fd1e9..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-forbes", - templateUrl: "logo-forbes.component.html", -}) -export class LogoForbesComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html deleted file mode 100644 index bd44b56f090..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html +++ /dev/null @@ -1,5 +0,0 @@ -US News 360 Reviews Best Password Manager diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts deleted file mode 100644 index fb0b1e0c71b..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-us-news", - templateUrl: "logo-us-news.component.html", -}) -export class LogoUSNewsComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html deleted file mode 100644 index cd719a35af8..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

- {{ header }} -

-
- "{{ quote }}" -
-
- -

{{ source }}

-
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts deleted file mode 100644 index 6419ddf1e45..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -@Component({ - selector: "app-review-blurb", - templateUrl: "review-blurb.component.html", -}) -export class ReviewBlurbComponent { - @Input() header: string; - @Input() quote: string; - @Input() source: string; -} diff --git a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html deleted file mode 100644 index 77f592f1c45..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- -
-
- - - - -
-
- -
- -
-
- 4.7 -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts deleted file mode 100644 index 9b104ac0bc3..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -@Component({ - selector: "review-logo", - templateUrl: "review-logo.component.html", -}) -export class ReviewLogoComponent { - @Input() logoClass: string; - @Input() logoSrc: string; - @Input() logoAlt: string; -} diff --git a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html deleted file mode 100644 index 569ff91f625..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html +++ /dev/null @@ -1,30 +0,0 @@ -

{{ header }}

-
-

- {{ headline }} -

-
-
    -
  • - {{ primaryPoint }} -
  • -
-
-
-
-

{{ calloutHeadline }}

-
    -
  • - {{ callout }} -
  • -
-
-
-
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts deleted file mode 100644 index 955c18fddf2..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -@Component({ - selector: "app-secrets-manager-content", - templateUrl: "secrets-manager-content.component.html", -}) -export class SecretsManagerContentComponent implements OnInit, OnDestroy { - header: string; - headline = - "A simpler, faster way to secure and automate secrets across code and infrastructure deployments"; - primaryPoints: string[]; - calloutHeadline: string; - callouts: string[]; - - private paidPrimaryPoints = [ - "Unlimited secrets, users, and projects", - "Simple and transparent pricing", - "Zero-knowledge, end-to-end encryption", - ]; - - private paidCalloutHeadline = "Limited time offer"; - - private paidCallouts = [ - "Sign up today and receive a complimentary 12-month subscription to Bitwarden Password Manager", - "Experience complete security across your organization", - "Secure all your sensitive credentials, from user applications to machine secrets", - ]; - - private freePrimaryPoints = [ - "Unlimited secrets", - "Simple and transparent pricing", - "Zero-knowledge, end-to-end encryption", - ]; - - private freeCalloutHeadline = "Go beyond developer security!"; - - private freeCallouts = [ - "Your Bitwarden account will also grant complimentary access to Bitwarden Password Manager", - "Extend end-to-end encryption to your personal passwords, addresses, credit cards and notes", - ]; - - private destroy$ = new Subject(); - - constructor(private activatedRoute: ActivatedRoute) {} - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - ngOnInit(): void { - this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => { - switch (queryParameters.org) { - case "enterprise": - this.header = "Secrets Manager for Enterprise"; - this.primaryPoints = this.paidPrimaryPoints; - this.calloutHeadline = this.paidCalloutHeadline; - this.callouts = this.paidCallouts; - break; - case "free": - this.header = "Bitwarden Secrets Manager"; - this.primaryPoints = this.freePrimaryPoints; - this.calloutHeadline = this.freeCalloutHeadline; - this.callouts = this.freeCallouts; - break; - case "teams": - case "teamsStarter": - this.header = "Secrets Manager for Teams"; - this.primaryPoints = this.paidPrimaryPoints; - this.calloutHeadline = this.paidCalloutHeadline; - this.callouts = this.paidCallouts; - break; - } - }); - } -} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts deleted file mode 100644 index 5c97695deff..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams-content", - templateUrl: "teams-content.component.html", -}) -export class TeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html deleted file mode 100644 index f51c370bebd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

Start your 7-day free trial for Teams

-
-

- Strengthen business security with an easy-to-use password manager your team will love. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Enhance security practices across your team with easy user management -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts deleted file mode 100644 index 055ec7fda10..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams1-content", - templateUrl: "teams1-content.component.html", -}) -export class Teams1ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html deleted file mode 100644 index f51c370bebd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

Start your 7-day free trial for Teams

-
-

- Strengthen business security with an easy-to-use password manager your team will love. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Enhance security practices across your team with easy user management -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts deleted file mode 100644 index 394ba90b491..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams2-content", - templateUrl: "teams2-content.component.html", -}) -export class Teams2ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html deleted file mode 100644 index c6f1ae697ae..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html +++ /dev/null @@ -1,26 +0,0 @@ -

Begin Teams Starter Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • - Powerful security for up to 10 users -
    - Have more than 10 users? - Start a Teams trial -
    -
  • -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts deleted file mode 100644 index df91268ab26..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams3-content", - templateUrl: "teams3-content.component.html", -}) -export class Teams3ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html deleted file mode 100644 index dddac598a46..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - -
-

{{ "smFreeTrialThankYou" | i18n }}

-
    -
  • -

    - {{ "smFreeTrialConfirmationEmail" | i18n }} - {{ formGroup.get("email").value }}. -

    -
  • -
-
-
- - -
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts deleted file mode 100644 index f7c5a9b2b98..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; - -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component"; - -@Component({ - selector: "app-secrets-manager-trial-free-stepper", - templateUrl: "secrets-manager-trial-free-stepper.component.html", -}) -export class SecretsManagerTrialFreeStepperComponent implements OnInit { - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - - formGroup = this.formBuilder.group({ - name: [ - "", - { - validators: [Validators.required, Validators.maxLength(50)], - updateOn: "change", - }, - ], - email: [ - "", - { - validators: [Validators.email], - }, - ], - }); - - subLabels = { - createAccount: - "Before creating your free organization, you first need to log in or create a personal account.", - organizationInfo: "Enter your organization information", - }; - - organizationId: string; - - referenceEventRequest: ReferenceEventRequest; - - constructor( - protected formBuilder: UntypedFormBuilder, - protected i18nService: I18nService, - protected organizationBillingService: OrganizationBillingService, - protected router: Router, - ) {} - - ngOnInit(): void { - this.referenceEventRequest = new ReferenceEventRequest(); - this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; - } - - accountCreated(email: string): void { - this.formGroup.get("email")?.setValue(email); - this.subLabels.createAccount = email; - this.verticalStepper.next(); - } - - async createOrganization(): Promise { - const response = await this.organizationBillingService.startFree({ - organization: { - name: this.formGroup.get("name").value, - billingEmail: this.formGroup.get("email").value, - }, - plan: { - type: PlanType.Free, - subscribeToSecretsManager: true, - isFromSecretsManagerTrial: true, - }, - }); - - this.organizationId = response.id; - this.subLabels.organizationInfo = response.name; - this.verticalStepper.next(); - } - - async navigateToMembers(): Promise { - await this.router.navigate(["organizations", this.organizationId, "members"]); - } - - async navigateToSecretsManager(): Promise { - await this.router.navigate(["sm", this.organizationId]); - } -} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html deleted file mode 100644 index 99e2706d713..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - -
- - -
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts deleted file mode 100644 index 650c1d8e69e..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component"; -import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component"; - -export enum ValidOrgParams { - families = "families", - enterprise = "enterprise", - teams = "teams", - teamsStarter = "teamsStarter", - individual = "individual", - premium = "premium", - free = "free", -} - -const trialFlowOrgs = [ - ValidOrgParams.teams, - ValidOrgParams.teamsStarter, - ValidOrgParams.enterprise, - ValidOrgParams.families, -]; - -@Component({ - selector: "app-secrets-manager-trial-paid-stepper", - templateUrl: "secrets-manager-trial-paid-stepper.component.html", -}) -export class SecretsManagerTrialPaidStepperComponent - extends SecretsManagerTrialFreeStepperComponent - implements OnInit -{ - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - @Input() organizationTypeQueryParameter: string; - - plan: PlanType; - createOrganizationLoading = false; - billingSubLabel = this.i18nService.t("billingTrialSubLabel"); - organizationId: string; - - private destroy$ = new Subject(); - protected enableTrialPayment$ = this.configService.getFeatureFlag$( - FeatureFlag.TrialPaymentOptional, - ); - - constructor( - private route: ActivatedRoute, - private configService: ConfigService, - protected formBuilder: UntypedFormBuilder, - protected i18nService: I18nService, - protected organizationBillingService: OrganizationBillingService, - protected router: Router, - ) { - super(formBuilder, i18nService, organizationBillingService, router); - } - - async ngOnInit(): Promise { - this.referenceEventRequest = new ReferenceEventRequest(); - this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; - - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { - if (trialFlowOrgs.includes(qParams.org)) { - if (qParams.org === ValidOrgParams.teamsStarter) { - this.plan = PlanType.TeamsStarter; - } else if (qParams.org === ValidOrgParams.teams) { - this.plan = PlanType.TeamsAnnually; - } else if (qParams.org === ValidOrgParams.enterprise) { - this.plan = PlanType.EnterpriseAnnually; - } - } - }); - } - - organizationCreated(event: OrganizationCreatedEvent) { - this.organizationId = event.organizationId; - this.billingSubLabel = event.planDescription; - this.verticalStepper.next(); - } - - steppedBack() { - this.verticalStepper.previous(); - } - - async createOrganizationOnTrial(): Promise { - this.createOrganizationLoading = true; - const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ - organization: { - name: this.formGroup.get("name").value, - billingEmail: this.formGroup.get("email").value, - initiationPath: "Secrets Manager trial from marketing website", - }, - plan: { - type: this.plan, - subscribeToSecretsManager: true, - isFromSecretsManagerTrial: true, - passwordManagerSeats: 1, - secretsManagerSeats: 1, - }, - }); - - this.organizationId = response?.id; - this.subLabels.organizationInfo = response?.name; - this.createOrganizationLoading = false; - this.verticalStepper.next(); - } - - get createAccountLabel() { - const organizationType = - this.productType === ProductTierType.TeamsStarter - ? "Teams Starter" - : ProductTierType[this.productType]; - return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`; - } - - get productType(): TrialOrganizationType { - switch (this.organizationTypeQueryParameter) { - case "enterprise": - return ProductTierType.Enterprise; - case "families": - return ProductTierType.Families; - case "teams": - return ProductTierType.Teams; - case "teamsStarter": - return ProductTierType.TeamsStarter; - } - } - - protected readonly SubscriptionProduct = SubscriptionProduct; -} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html deleted file mode 100644 index 88251136dbe..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html +++ /dev/null @@ -1,44 +0,0 @@ - - -
-
-
- Bitwarden -
- -
-
-
-
-
-
-

- {{ - "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" - | i18n: organizationTypeQueryParameter - }} -

- -
- - -
-
-
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts deleted file mode 100644 index 678514532ca..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -@Component({ - selector: "app-secrets-manager-trial", - templateUrl: "secrets-manager-trial.component.html", -}) -export class SecretsManagerTrialComponent implements OnInit, OnDestroy { - organizationTypeQueryParameter: string; - - private destroy$ = new Subject(); - - constructor(private route: ActivatedRoute) {} - - ngOnInit(): void { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => { - this.organizationTypeQueryParameter = queryParameters.org; - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get freeOrganization() { - return this.organizationTypeQueryParameter === "free"; - } -} diff --git a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts index 3e6bfdc4e6c..06e1cce7f23 100644 --- a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts @@ -7,36 +7,10 @@ import { FormFieldModule } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component"; -import { SecretsManagerTrialPaidStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component"; -import { SecretsManagerTrialComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial.component"; -import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module"; import { SharedModule } from "../../shared"; import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component"; import { ConfirmationDetailsComponent } from "./confirmation-details.component"; -import { AbmEnterpriseContentComponent } from "./content/abm-enterprise-content.component"; -import { AbmTeamsContentComponent } from "./content/abm-teams-content.component"; -import { CnetEnterpriseContentComponent } from "./content/cnet-enterprise-content.component"; -import { CnetIndividualContentComponent } from "./content/cnet-individual-content.component"; -import { CnetTeamsContentComponent } from "./content/cnet-teams-content.component"; -import { DefaultContentComponent } from "./content/default-content.component"; -import { EnterpriseContentComponent } from "./content/enterprise-content.component"; -import { Enterprise1ContentComponent } from "./content/enterprise1-content.component"; -import { Enterprise2ContentComponent } from "./content/enterprise2-content.component"; -import { LogoBadgesComponent } from "./content/logo-badges.component"; -import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component"; -import { LogoCnetComponent } from "./content/logo-cnet.component"; -import { LogoCompanyTestimonialComponent } from "./content/logo-company-testimonial.component"; -import { LogoForbesComponent } from "./content/logo-forbes.component"; -import { LogoUSNewsComponent } from "./content/logo-us-news.component"; -import { ReviewBlurbComponent } from "./content/review-blurb.component"; -import { ReviewLogoComponent } from "./content/review-logo.component"; -import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component"; -import { TeamsContentComponent } from "./content/teams-content.component"; -import { Teams1ContentComponent } from "./content/teams1-content.component"; -import { Teams2ContentComponent } from "./content/teams2-content.component"; -import { Teams3ContentComponent } from "./content/teams3-content.component"; import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module"; @NgModule({ @@ -46,41 +20,10 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul VerticalStepperModule, FormFieldModule, OrganizationCreateModule, - EnvironmentSelectorModule, TrialBillingStepComponent, InputPasswordComponent, ], - declarations: [ - CompleteTrialInitiationComponent, - EnterpriseContentComponent, - TeamsContentComponent, - ConfirmationDetailsComponent, - DefaultContentComponent, - EnterpriseContentComponent, - Enterprise1ContentComponent, - Enterprise2ContentComponent, - TeamsContentComponent, - Teams1ContentComponent, - Teams2ContentComponent, - Teams3ContentComponent, - CnetEnterpriseContentComponent, - CnetIndividualContentComponent, - CnetTeamsContentComponent, - AbmEnterpriseContentComponent, - AbmTeamsContentComponent, - LogoBadgesComponent, - LogoCnet5StarsComponent, - LogoCompanyTestimonialComponent, - LogoCnetComponent, - LogoForbesComponent, - LogoUSNewsComponent, - ReviewLogoComponent, - SecretsManagerContentComponent, - ReviewBlurbComponent, - SecretsManagerTrialComponent, - SecretsManagerTrialFreeStepperComponent, - SecretsManagerTrialPaidStepperComponent, - ], + declarations: [CompleteTrialInitiationComponent, ConfirmationDetailsComponent], exports: [CompleteTrialInitiationComponent], providers: [TitleCasePipe], }) diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index 18ee9462f4f..dac5afa191a 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -183,7 +183,10 @@ describe("KeyRotationService", () => { mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash"); mockConfigService.getFeatureFlag.mockResolvedValue(true); - mockEncryptService.encrypt.mockResolvedValue({ + mockEncryptService.wrapSymmetricKey.mockResolvedValue({ + encryptedString: "mockEncryptedData", + } as any); + mockEncryptService.wrapDecapsulationKey.mockResolvedValue({ encryptedString: "mockEncryptedData", } as any); diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 7d00e970ad7..ec38f49baeb 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -145,7 +145,9 @@ export class UserKeyRotationService { const { privateKey, publicKey } = keyPair; const accountKeysRequest = new AccountKeysRequest( - (await this.encryptService.encrypt(privateKey, newUnencryptedUserKey)).encryptedString!, + ( + await this.encryptService.wrapDecapsulationKey(privateKey, newUnencryptedUserKey) + ).encryptedString!, Utils.fromBufferToB64(publicKey), ); @@ -427,6 +429,6 @@ export class UserKeyRotationService { if (privateKey == null) { throw new Error("No private key found for user key rotation"); } - return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString; + return (await this.encryptService.wrapDecapsulationKey(privateKey, newUserKey)).encryptedString; } } diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts index 3c941fe24c7..9e993259830 100644 --- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts @@ -52,6 +52,22 @@ describe("WebLockComponentService", () => { }); }); + describe("popOutBrowserExtension", () => { + it("throws platform not supported error", () => { + expect(() => service.popOutBrowserExtension()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + + describe("closeBrowserExtensionPopout", () => { + it("throws platform not supported error", () => { + expect(() => service.closeBrowserExtensionPopout()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + describe("isWindowVisible", () => { it("throws an error", async () => { await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts index dd9f5138dba..ea038ca2c67 100644 --- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts @@ -24,6 +24,14 @@ export class WebLockComponentService implements LockComponentService { return null; } + popOutBrowserExtension(): Promise { + throw new Error("Method not supported on this platform."); + } + + closeBrowserExtensionPopout(): void { + throw new Error("Method not supported on this platform."); + } + async isWindowVisible(): Promise { throw new Error("Method not implemented."); } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 10466503029..99159e7e2fc 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -7,6 +7,7 @@ import { firstValueFrom, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -39,6 +40,9 @@ import { ToastService, } from "@bitwarden/components"; import { + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, ChangeLoginPasswordService, CipherFormComponent, CipherFormConfig, @@ -50,16 +54,10 @@ import { } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; -import { - AttachmentDialogCloseResult, - AttachmentDialogResult, - AttachmentsV2Component, -} from "../../individual-vault/attachments-v2.component"; +import { WebVaultPremiumUpgradePromptService } from "../../../vault/services/web-premium-upgrade-prompt.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; -import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; -import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; export type VaultItemDialogMode = "view" | "form"; @@ -135,7 +133,7 @@ export enum VaultItemDialogResult { ], providers: [ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, RoutedVaultFilterService, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index d9b42594d79..2eab6faec36 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -21,6 +21,7 @@ import { ItemModule, } from "@bitwarden/components"; import { + AttachmentsV2Component, CipherAttachmentsComponent, CipherFormConfig, CipherFormGenerationService, @@ -31,8 +32,6 @@ import { import { SharedModule } from "../../shared/shared.module"; import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service"; -import { AttachmentsV2Component } from "./attachments-v2.component"; - /** * The result of the AddEditCipherDialogV2 component. */ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 7055f164a53..5f56ecc9e04 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -69,6 +69,9 @@ import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/compon import { AddEditFolderDialogComponent, AddEditFolderDialogResult, + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, CipherFormConfig, CollectionAssignmentResult, DecryptionFailureDialogComponent, @@ -96,11 +99,6 @@ import { VaultItem } from "../components/vault-items/vault-item"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; -import { - AttachmentDialogCloseResult, - AttachmentDialogResult, - AttachmentsV2Component, -} from "./attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 3e5cced7fa8..e7b06cbb8d6 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -5,6 +5,7 @@ import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; import { firstValueFrom, map, Observable } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -21,8 +22,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DIALOG_DATA, - DialogConfig, DialogRef, + DialogConfig, AsyncActionsModule, DialogModule, DialogService, @@ -31,8 +32,7 @@ import { import { CipherViewComponent } from "@bitwarden/vault"; import { SharedModule } from "../../shared/shared.module"; -import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; -import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service"; +import { WebVaultPremiumUpgradePromptService } from "../../vault/services/web-premium-upgrade-prompt.service"; export interface ViewCipherDialogParams { cipher: CipherView; @@ -74,7 +74,7 @@ export interface ViewCipherDialogCloseResult { standalone: true, imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule], providers: [ - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, ], }) diff --git a/apps/web/src/connectors/webauthn-fallback.ts b/apps/web/src/connectors/webauthn-fallback.ts index 3561f922e03..43be5733973 100644 --- a/apps/web/src/connectors/webauthn-fallback.ts +++ b/apps/web/src/connectors/webauthn-fallback.ts @@ -86,7 +86,7 @@ document.addEventListener("DOMContentLoaded", async () => { titleForLargerScreens.innerText = localeService.t("verifyYourIdentity"); const subtitle = document.getElementById("subtitle"); - subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingIn"); + subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"); }); function start() { diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 56eabc34863..da0d698cfc3 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Werk Blaaier By" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "U gebruik ’n onondersteunde webblaaier. Die webkluis werk dalk nie soos normaal nie." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Gratis Bitwarden Gesinne" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "U en u gesin kom in aanmerking vir gratis Bitwarden Gesinne. Los af met u persoonlike e-pos om u data veilig te hou selfs wanneer u nie op kantoor is nie." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Gedeelde versamelings vir gesinsgeheime" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Hierdie skakel is nie meer geldig nie. Laat die borg die aanbod weer stuur." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Nooi lid uit" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Moet bevestig word" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 9ebd9873655..2aa429dba3f 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 78b0b1ec825..0064c069ae4 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Brauzeri güncəllə" }, - "generatingRiskInsights": { - "message": "Risk təhlilləriniz yaradılır..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Dəstəklənməyən bir veb brauzer istifadə edirsiniz. Veb seyf düzgün işləməyə bilər." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Ödənişsiz Bitwarden Ailələri" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Siz və ailəniz Ödənişsiz Bitwarden Ailələri üçün uyğunsunuz. İşdə olmadığınız vaxtlarda belə datanızı güvənli saxlamaq üçün özəl e-poçtunuzu istifadə edin." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Ailə sirləri üçün paylaşılan kolleksiyalar" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Keçid, artıq etibarlı deyil. Lütfən sponsorun təklifi yenidən göndərməsini təmin edin." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Üzv dəvət et" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Təsdiqləmə lazımdır" }, diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 0b0b405721f..f812a4642a2 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Абнавіць браўзер" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Ваш браўзер не падтрымліваецца. Вэб-сховішча можа працаваць няправільна." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Бясплатны тарыфны план Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Вы і ваша сям'я маеце права на бясплатны тарыфны план Bitwarden Families. Актывуйце доступ з асабістай электроннай поштай, каб захаваць свае даныя ў бяспецы, нават калі вы не на працы." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Абагуленыя калекцыі для сямейных сакрэтаў" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Спасылка больш не дзейнічае. Калі ласка, папрасіце спонсара паўторна адправіць прапанову." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Запрасіць удзельніка" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Патрабуецца пацвярджэнне" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 13bbdc14d4a..fc33ba64843 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Обновяване на браузъра" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Създаване на Вашата информация относно рисковете…" }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Споделени колекции за семейни тайни" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Връзката е с изтекла давност. Помолете спонсора да изпрати отново предложението." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Покана на член" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Изисква потвърждение" }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index dc879696b3c..dac995ecc2c 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index aea556cde36..2eeb7cf9c93 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 93fa6e5793b..340d8b68117 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Actualitza el navegador" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Esteu utilitzant un navegador web no compatible. La caixa forta web pot no funcionar correctament." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Famílies Bitwarden gratuït" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Tu i la teua família sou elegibles per a famílies Bitwarden gratuït. Bescanvia amb el correu electrònic personal per mantenir les dades segures fins i tot quan no esteu a la feina." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Col·leccions compartides de Secrets familiars" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "L'enllaç ja no és vàlid. Fes que el patrocinador torne a enviar l'oferta." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Convida membre" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Necessita confirmació" }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 35b0904bda2..0aa8e8440d8 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Aktualizovat prohlížeč" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Generování poznatků o rizicích..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden pro rodinu zdarma" }, + "sponsoredBitwardenFamilies": { + "message": "Sponzorované rodiny" + }, + "noSponsoredFamilies": { + "message": "Žádné sponzorované rodiny" + }, + "noSponsoredFamiliesDescription": { + "message": "Zde se zobrazí sponzorované plány rodin, které nejsou členy" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Členové vaší organizace mají nárok na bezplatné rodiny Bitwarden. Můžete sponzorovat zdarma rodiny Bitwarden pro zaměstnance, kteří nejsou členy Vaší organizace Bitwarden. Sponzorování nečlena vyžaduje dostupné místo ve Vaší organizaci." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "Pokud odeberete aktivní sponzorství, bude po datu obnovení sponzorované organizace k dispozici volný uživatel ve Vaší organizaci." + }, "sponsoredFamiliesEligible": { "message": "Vy a Vaše rodina máte nárok na Bitwarden pro rodinu zdarma. Nárok můžete uplatnit Vaším osobním e-mailem, abyste zajistili bezpečnost Vašich dat, i když nejste v práci." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Sdílené kolekce pro tajné klíče rodiny" }, + "memberFamilies": { + "message": "Rodiny členů" + }, + "noMemberFamilies": { + "message": "Žádné rodiny členů" + }, + "noMemberFamiliesDescription": { + "message": "Zde se zobrazí členové, kteří využili rodinné plány" + }, + "membersWithSponsoredFamilies": { + "message": "Členové Vaší organizace mají nárok na bezplatné rodiny Bitwarden. Zde můžete vidět členy, kteří sponzorovali organizaci rodin." + }, "badToken": { "message": "Odkaz již není platný. Požádejte sponzora o opakované odeslání nabídky." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Pozvat uživatele" }, + "addSponsorship": { + "message": "Přidat sponzorství" + }, "needsConfirmation": { "message": "Vyžaduje potvrzení" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 69217c7f17b..a320fbb5756 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 9999432c50d..8c9df167884 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Opdatér browser" }, - "generatingRiskInsights": { - "message": "Genererer risikoindsigter..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Du bruger en ikke-understøttet webbrowser. Web-boksen fungerer muligvis ikke korrekt." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Gratis Bitwarden Familier" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Du og din familie er berettiget til gratis Bitwarden Familier. Indløs med din personlige e-mail for at holde dine data sikre, selv når du ikke er på arbejde." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Delte samlinger til Familiehemmeligheder" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Linket er ikke længere gyldigt. Bed sponsoren sende et nyt tilbud." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invitér medlem" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Behøver bekræftelse" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index a517629a5b4..41ff54f8203 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Browser aktualisieren" }, - "generatingRiskInsights": { - "message": "Dein Risiko-Überblick wird generiert..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Du verwendest einen nicht unterstützten Webbrowser. Der Web-Tresor funktioniert möglicherweise nicht richtig." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Kostenloses Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Du und deine Familie haben Anspruch auf Free Bitwarden Families. Mit deiner persönlichen E-Mail-Adresse einlösen, um deine Daten zu schützen, auch wenn du nicht am Arbeitsplatz bist." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Gemeinsame Sammlungen für Familiengeheimnisse" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Der Link ist nicht mehr gültig. Bitte lasse dir vom Sponsor das Angebot erneut senden." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Mitglied einladen" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Bestätigung erforderlich" }, diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index b44892b1276..32e169867f8 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Ενημερώστε τον Browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Χρησιμοποιείτε ένα μη υποστηριζόμενο browser. Το web vault ενδέχεται να μην λειτουργεί σωστά." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Δωρεάν Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Εσείς και η οικογένεια σας έχετε δικαίωμα για Δωρεάν Bitwarden Families. Εξαργυρώστε με το προσωπικό σας email για να διατηρείτε τα δεδομένα σας ασφαλή ακόμα και όταν δεν εργάζεστε." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Κοινές συλλογές για οικογενειακά μυστικά" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Ο σύνδεσμος δεν είναι πλέον έγκυρος. Παρακαλώ στείλτε ξανά την προσφορά." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Πρόσκληση μέλους" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Χρειάζεται επιβεβαίωση" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 56a98a661ef..05d29071731 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7280,6 +7280,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "launchDuo": { "message": "Launch Duo" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index daef0bc6945..91d19c14186 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 0804aa60e08..c110b0405c9 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 33afe669868..e4f6af64c43 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Ĝisdatigi retumilon" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Vi uzas nesubtenatan tTT-legilon. La ttt-volbo eble ne funkcias ĝuste." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index d3f9c608748..a2de480e314 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Actualizar navegador" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Está utilizando un navegador web no compatible. Es posible que la caja fuerte web no funcione correctamente." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Familias Bitwarden gratis" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Usted y su familia son elegibles para Familias Bitwarden Gratis. Canjeé con su correo electrónico personal para mantener tus datos seguros incluso cuando no estés en el trabajo." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Colecciones compartidas para Secretos Familiares" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "El enlace ya no es válido. Por favor, solicite al patrocinados que vuelva a enviar la oferta." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invitar miembro" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Necesita confirmación" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index b5ed1f22f61..4d830c453ec 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Uuenda brauserit" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Kasutad brauserit, mida ei toetata. Veebihoidla ei pruugi hästi töötada." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Tasuta Bitwarden Families pakett" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Sina ja su pere olete valitud tasuta Bitwarden Families paketi saamiseks. Lunasta see läbi oma emaili, et hoida oma andmed ohutus kohas isegi kui sa ei ole tööl." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Jagatud kogumikud pere saladuste talletamiseks" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "See link ei ole enam aktiivne. Uue lingi saamiseks kontakteeruge oma sponsoriga, et ta saadaks pakkumise uuesti." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 06a13bb6765..0f8858914b4 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Nabigatzailea eguneratu" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Euskarririk gabeko web nabigatzailea erabiltzen ari zara. Baliteke webguneko kutxa gotorrak behar bezala ez funtzionatzea." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Doako Bitwarden Familia" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Zu eta zure familia hautagai zarete Free Bitwarden Familia-rentzat. Trukatu zure email pertsonalarekin zure datuak modu seguruan." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Familiako sekretuen bilduma partekatuak" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Lotura ez da baliozkoa. Mesedez, eskatu babesleari eskaintza birbidaltzeko." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 5e37f707f0c..faee8b5480f 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "به‌روزرسانی مرورگر" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "شما از یک مرورگر وب پشتیبانی نشده استفاده می‌کنید. گاوصندوق وب ممکن است به درستی کار نکند." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "خانواده‌های Bitwarden رایگان" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "شما و خانواده‌تان واجد شرایط دریافت خانواده‌های Bitwarden رایگان هستید. با ایمیل شخصی خود بازخرید کنید تا اطلاعات خود را حتی زمانی که در محل کار نیستید ایمن نگه دارید." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "مجموعه‌های مشترک برای اسرار خانواده" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "پیوند دیگر معتبر نیست. لطفاً از حمایت کننده بخواهید پیشنهاد را مجدداً ارسال کند." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "دعوت از عضو" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "نیاز به تأیید دارد" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index b5b5108aa14..417ac391763 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -1397,7 +1397,7 @@ "message": "Unlock Bitwarden on your device or on the " }, "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" + "message": "Yritätkö käyttää tiliäsi?" }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Päivitä selain" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Käytät selainta, jota ei tueta. Verkkoholvi ei välttämättä toimi oikein." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Ilmainen Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Sinä ja perheesi olette oikeutettuja ilmaiseen Bitwarden Families -tilaukseen. Lunasta tarjous henkilökohtaisella sähköpostiosoitteellasi suojataksesi tietosi myös töiden ulkopuolella." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Jaetut kokoelmat perheen salaisuuksille" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Linkki ei ole enää voimassa. Pyydä sponsoria lähettämään tarjous uudelleen." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Kutsu jäsen" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Vahvistettavat" }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 04f97960ccf..fc01382f527 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update sa browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Gumagamit ka ng isang hindi suportado na web browser. Ang web vault ay maaaring hindi gumana nang maayos." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Libreng Mga Pamilya ng Bitwarden" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Ikaw at ang iyong pamilya ay karapat dapat para sa Free Bitwarden Families. Tubusin gamit ang iyong personal na email upang mapanatili ang iyong data na ligtas kahit na wala ka sa trabaho." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Ibinahagi ang mga koleksyon para sa mga lihim ng Pamilya" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Hindi na valid ang link. Paki resend na lang sa sponsor ang offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Mag imbita ng miyembro" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Kailangan ng kumpirmasyon" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 0f6969aa333..70ac93232e8 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Mettre à jour le navigateur" }, - "generatingRiskInsights": { - "message": "Génération de vos connaissances en matière de risque..." + "generatingYourRiskInsights": { + "message": "Génération de vos Aperçus de Risque..." }, "updateBrowserDesc": { "message": "Vous utilisez un navigateur non supporté. Le coffre web pourrait ne pas fonctionner correctement." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden Familles gratuit" }, + "sponsoredBitwardenFamilies": { + "message": "Familles parrainées" + }, + "noSponsoredFamilies": { + "message": "Aucune famille parrainée" + }, + "noSponsoredFamiliesDescription": { + "message": "Les abonnements des familles non membres parrainées apparaîtront ici" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Les membres de votre organisation sont éligibles pour les Familles Gratuites de Bitwarden. Vous pouvez parrainer Familles Gratuites de Bitwarden pour les employés qui ne sont pas membres de votre organisation Bitwarden. Parrainer un non-membre nécessite un siège disponible au sein de votre organisation." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "Lorsque vous supprimez un parrainage actif, une licence au sein de votre organisation sera disponible après la date de renouvellement de l'organisation parrainée." + }, "sponsoredFamiliesEligible": { "message": "Vous et votre famille êtes éligibles à Bitwarden Familles gratuitement. Réclamez-le avec votre courriel personnel pour sécuriser vos données, même lorsque vous n'êtes pas au travail." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Collections partagées pour les secrets de la famille" }, + "memberFamilies": { + "message": "Familles de membre" + }, + "noMemberFamilies": { + "message": "Familles de non-membre" + }, + "noMemberFamiliesDescription": { + "message": "Les membres qui ont réclamé des plans familiaux s'afficheront ici" + }, + "membersWithSponsoredFamilies": { + "message": "Les membres de votre organisation sont éligibles à l'abonnement aux Familles Gratuites de Bitwarden. Vous pouvez voir ici les membres qui ont parrainé une organisation Familles." + }, "badToken": { "message": "Le lien n'est plus valide. Merci de demander à votre parrain de renvoyer l'offre." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Inviter un membre" }, + "addSponsorship": { + "message": "Ajouter un parrainage" + }, "needsConfirmation": { "message": "Confirmation nécessaire" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 33aec61c9ad..cd8c389a0b2 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index ee7f464b9fd..04cbec113ec 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "עדכן דפדפן" }, - "generatingRiskInsights": { - "message": "יוצר את תובנות הסיכון שלך..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "אתה משתמש בדפדפן אינטרנט שאיננו נתמך. כספת הרשת עלולה שלא לפעול כראוי." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden למשפחות בחינם" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "אתה והמשפחה שלך זכאים ל־Bitwarden למשפחות בחינם. ממש עם הדוא\"ל האישי שלך כדי לשמור על אבטחת הנתונים שלך אפילו כשאתה לא בעבודה." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "אוספים משותפים עבור סודות משפחה" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "הקישור אינו חוקי עוד. אנא בקש מנותן החסות לשלוח שוב את ההצעה." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "הזמן חבר" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "צריך אישור" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 2ffc414866e..8a55f388919 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index e5121050ed8..d1a773b20ce 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Ažuriraj preglednik" }, - "generatingRiskInsights": { - "message": "Stvaranje tvojih uvida u rizik..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Koristiš nepodržani preglednik. Web trezor možda neće ispravno raditi." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Besplatan Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Ti i tvoja obitelj ispunjavate uvjete za besplatni Bitwarden Families. Iskoristiti ponudu svojom e-poštom kako bi zaštitio svoje podatke čak i kada nisi na poslu." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Dijeljene zbirke za obiteljske tajne" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Veza više nije valjana. Zamoli sponzora da pošalje novu ponudu." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Pozovi člana" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Treba potvrdu" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 266783e836f..a0ff38cf02d 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Böngésző frissítése" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "A kockázati betekintések generálása..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Díjmentes Bitwarden családi csomag" }, + "sponsoredBitwardenFamilies": { + "message": "Szponzorált családok" + }, + "noSponsoredFamilies": { + "message": "Nincsenek szponzorált családok." + }, + "noSponsoredFamiliesDescription": { + "message": "A szponzorált, nem-tag családok csomagjai itt jelennek meg." + }, + "sponsorFreeBitwardenFamilies": { + "message": "A szervezet tagjai jogosultak a Free Bitwarden Families programra. Ingyenes Bitwarden Családokat szponzorálhat olyan alkalmazottak számára, akik nem tagjai Bitwarden szervezetének. A nem-tag szponzorálásához a szervezeten belül rendelkezésre álló hely szükséges." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "Amikor eltávolítunk egy aktív szponzorációt, a szponzorált szervezet megújítási dátuma után elérhető lesz egy hely a szervezeten belül." + }, "sponsoredFamiliesEligible": { "message": "A felhasználó és családja jogosult az ingyenes Bitwarden családok programra. Váltsuk ezt be személyes email címmel, hogy az adatok biztonságban legyenek még akkor is, amikor éppen nem dolgozunk." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Megosztott gyűjtemények a családi titkoknak" }, + "memberFamilies": { + "message": "Tag családok" + }, + "noMemberFamilies": { + "message": "Nincsenek tag családok." + }, + "noMemberFamiliesDescription": { + "message": "Itt jelennek meg azok a tagok, akik beváltották a családi csomagokat." + }, + "membersWithSponsoredFamilies": { + "message": "A szervezet tagjai jogosultak a Free Bitwarden Families programra. Itt láthatjuk azokat a tagokat, akik szponzoráltak egy Családok szervezetet." + }, "badToken": { "message": "A hivatkozás már nem érvényes. Kérjük a szponzortól az ajánlat újraküldését." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Tag meghívása" }, + "addSponsorship": { + "message": "Szponzorálás hozzáadása" + }, "needsConfirmation": { "message": "Jóváhagyás szükséges" }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 85d6795e9c1..753c865357c 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Perbarui Browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Anda menggunakan browser web yang tidak didukung. Kubah web mungkin tidak berfungsi dengan baik." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden Families Gratis" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 893eed1b846..2bb57c594d3 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Aggiorna browser" }, - "generatingRiskInsights": { - "message": "Generazione delle tue informazioni sui rischi..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Stai utilizzando un browser non supportato. La cassaforte web potrebbe non funzionare correttamente." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden Families gratis" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Tu e la tua famiglia siete idonei per Bitwarden Families gratis. Riscatta con la tua email personale per mantenere i tuoi dati al sicuro anche quando non sei a lavoro." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Raccolte condivise per segreti di famiglia" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Il link non è più valido. Chiedi allo sponsor di inviare l'offerta di nuovo." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invita membro" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Necessitano conferma" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index b9d113b98cd..4463a08d766 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "ブラウザを更新" }, - "generatingRiskInsights": { - "message": "リスク分析を生成しています..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "サポートされていないブラウザを使用しています。ウェブ保管庫が正しく動作しないかもしれません。" @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden 家族向けプランを無償で" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "あなたとご家族は、 Bitwarden 家族向けプランを無償でご利用いただけます。 個人用のメールアドレスで引き換えて、仕事以外でもデータを安全に保ちましょう。" }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "家族のシークレットのために共有されたコレクション" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "リンクは無効になりました。スポンサーにオファーを再送信してもらってください。" }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "メンバーを招待する" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "確認が必要" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index ede2850a6e4..cf82a4b8b68 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 293b3b0486f..6a96c3529e2 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 9074ab87052..d1c3f149b45 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "ಬ್ರೌಸರ್ ನವೀಕರಿಸಿ" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "ನೀವು ಬೆಂಬಲಿಸದ ವೆಬ್ ಬ್ರೌಸರ್ ಅನ್ನು ಬಳಸುತ್ತಿರುವಿರಿ. ವೆಬ್ ವಾಲ್ಟ್ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸದೆ ಇರಬಹುದು." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index b0868c3e601..4d5dc07bdc5 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "브라우저 업데이트" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "지원하지 않는 웹 브라우저를 사용하고 있습니다. 웹 보관함 기능이 제대로 동작하지 않을 수 있습니다." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "구성원 초대" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 3faf2a549de..d7379dcb02e 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Atjaunināt pārlūku" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Tiek veidots ieskats par riskiem..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bezmaksas Bitwarden ģimenēm" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Tu un Tava ģimene esat atbilstīgi bezmaksas Bitwarden Families. Piesakies ar personīgo e-pasta adresi, lai turētu datus drošībā pat tad, kad neesi darbā!" }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Ģimenes noslēpumu kopīgotie krājumi" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Saite vairs nav derīga. Lūdz pabalstītāju atkārtoti nosūtīt piedāvājumu!" }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Uzaicināt dalībnieku" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Nepieciešams apstiprinājums" }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 620d1285e65..de418138e4e 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "ബ്രൌസർ അപ്‌ഡേറ്റുചെയ്യുക" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 293b3b0486f..6a96c3529e2 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 293b3b0486f..6a96c3529e2 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index c31140eb136..f9ca2ea0594 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Oppdater nettleseren" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Du bruker en ustøttet nettleser. Netthvelvet vil kanskje ikke fungere ordentlig." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Gratis Bitwarden Familier" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Du og din familie er kvalifisert for Free Bitwarden Familier. Løs inn med din personlige e-post for å holde dataene dine sikre, selv om du ikke er på jobb." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Delte samlinger til familie-hemmeligheter" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Lenken er ikke lenger gyldig. Vennligst ha sponsor på nytt tilbudet." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Inviter medlem" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Behøver bekreftelse" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 6052556f397..48560b0990d 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index b6811d9289f..4880e2bb211 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Webbrowser bijwerken" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Je risico-inzichten genereren..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Gratis Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Gesponsorde families" + }, + "noSponsoredFamilies": { + "message": "Geen gesponsorde families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Jij en je familie komen in aanmerking voor gratis Bitwarden Families. Verzilver met je persoonlijke e-mail om je gegevens veilig te houden, zelfs als je niet op het werk bent." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Gedeelde collecties voor familiegeheimen" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "De link is niet langer geldig. Zorg ervoor dat de sponsors de uitnodiging opnieuw versturen." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Lid uitnodigen" }, + "addSponsorship": { + "message": "Sponsoring toevoegen" + }, "needsConfirmation": { "message": "Bevestiging nodig" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 41d012ac3e9..42b425c13c4 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 293b3b0486f..6a96c3529e2 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 7fa157e523c..62e3aba491f 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Aktualizuj przeglądarkę" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Generowanie informacji o ryzyku..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Darmowy plan rodzinny" }, + "sponsoredBitwardenFamilies": { + "message": "Rodziny sponsorowane" + }, + "noSponsoredFamilies": { + "message": "Brak sponsorowanych rodzin" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsorowane plany rodzin niebędących członkami będą wyświetlane tutaj" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Członkowie Twojej organizacji kwalifikują się do Free Bitwarden Families. Możesz sponsorować Free Bitwarden Families dla pracowników, którzy nie są członkami Twojej organizacji Bitwarden. Sponsorowanie osoby niebędącej członkiem wymaga dostępnego miejsca w Twojej organizacji." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "Gdy aktywny sponsoring zostanie usunięty, miejsce w organizacji stanie się dostępne po dacie odnowienia sponsorowanej organizacji." + }, "sponsoredFamiliesEligible": { "message": "Ty i Twoja rodzina kwalifikujecie się do darmowego planu rodzinnego. Wykorzystaj swój osobisty adres e-mail, aby zabezpieczyć swoje dane nawet wtedy, gdy nie jesteś w pracy." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Udostępnione kolekcje dla sekretów rodziny" }, + "memberFamilies": { + "message": "Rodziny członków" + }, + "noMemberFamilies": { + "message": "Brak rodzin członków" + }, + "noMemberFamiliesDescription": { + "message": "Tutaj wyświetlą się członkowie, którzy wykupili plany rodzinne" + }, + "membersWithSponsoredFamilies": { + "message": "Członkowie Twojej organizacji kwalifikują się do Free Bitwarden Families. Tutaj możesz zobaczyć członków, którzy sponsorowali organizację rodzinną." + }, "badToken": { "message": "Link nie jest już ważny. Poproś sponsora o ponowne wysłanie oferty." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Zaproś członka" }, + "addSponsorship": { + "message": "Dodaj sponsorowanie" + }, "needsConfirmation": { "message": "Wymaga potwierdzenia" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 70cbc35fe6c..4ba7b5d8e5f 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Atualizar Navegador" }, - "generatingRiskInsights": { - "message": "Gerando seu panorama de risco..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Você está usando um navegador da Web não suportado. O cofre web pode não funcionar corretamente." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden Families Gratuito" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Você e sua família estão elegíveis para o Bitwarden Families Gratuito. Resgate com seu e-mail pessoal para manter seus dados seguros mesmo quando você não estiver no trabalho." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Coleções compartilhadas de segredos de Família" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "O link não é mais válido. Peça ao patrocinador para reenviar a oferta." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Convidar membro" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Precisa de confirmação" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 3840d5fd0fd..419e71709e3 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Atualizar navegador" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "A gerar as suas perceções de riscos..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Plano Familiar gratuito Bitwarden" }, + "sponsoredBitwardenFamilies": { + "message": "Planos familiares patrocinados" + }, + "noSponsoredFamilies": { + "message": "Sem planos familiares patrocinados" + }, + "noSponsoredFamiliesDescription": { + "message": "Os planos familiares patrocinados de não membros serão apresentados aqui" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Os membros da sua organização são elegíveis para o plano Familiar Bitwarden gratuito. Pode patrocinar planos Familiares Bitwarden gratuitos para empregados que não sejam membros da sua organização Bitwarden. O patrocínio de um não membro requer um lugar disponível na sua organização." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "Quando remover um patrocínio ativo, ficará disponível um lugar na sua organização após a data de renovação da organização patrocinada." + }, "sponsoredFamiliesEligible": { "message": "O utilizador e a sua família são elegíveis para o plano Familiar gratuito Bitwarden. Resgate com o seu e-mail pessoal para manter os seus dados seguros mesmo quando não estiver no trabalho." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Coleções partilhadas para segredos familiares" }, + "memberFamilies": { + "message": "Membros do plano familiar" + }, + "noMemberFamilies": { + "message": "Sem membros do plano familiar" + }, + "noMemberFamiliesDescription": { + "message": "Os membros que tenham resgatado planos familiares serão apresentados aqui" + }, + "membersWithSponsoredFamilies": { + "message": "Os membros da sua organização são elegíveis para os planos Familiares Bitwarden gratuitos. Aqui pode ver os membros que patrocinaram uma organização Familiar." + }, "badToken": { "message": "O link já não é válido. Peça ao patrocinador para reenviar a oferta." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Convidar membro" }, + "addSponsorship": { + "message": "Adicionar patrocínio" + }, "needsConfirmation": { "message": "Precisa de confirmação" }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index e513deb7f5e..a35d843d1ee 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Actualizare browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Utilizați un browser nesuportat. Seiful web ar putea să nu funcționeze corect." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Planul Bitwarden Familii gratuit" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Dumneavoastră și familia dvs., sunteți eligibili pentru planul Bitwarden Familii gratuit. Revendicați-l cu e-mailul personal pentru a vă păstra datele în siguranță chiar și atunci când nu sunteți la locul de muncă." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Colecții partajate pentru secrete familiale" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Linkul nu mai este valabil. Vă rugăm să cereți sponsorului să retrimită oferta." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index e84af0d0b54..740cd23bc9a 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Обновить браузер" }, - "generatingRiskInsights": { - "message": "Генерирование информации о рисках..." + "generatingYourRiskInsights": { + "message": "Генерация информации о рисках..." }, "updateBrowserDesc": { "message": "Вы используете неподдерживаемый браузер. Веб-хранилище может работать некорректно." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Бесплатный план Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Спонсируемые семьи" + }, + "noSponsoredFamilies": { + "message": "Нет спонсируемых семей" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Вам и вашей семье доступен бесплатный план Bitwarden Families. Используйте свой личный адрес электронной почты, чтобы защитить данные даже тогда, когда вы не на работе." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Общие коллекции для семейных секретов" }, + "memberFamilies": { + "message": "Члены семей" + }, + "noMemberFamilies": { + "message": "Нет членов семей" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Ссылка больше не действительна. Пожалуйста, попросите спонсора повторно отправить предложение." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Пригласить участника" }, + "addSponsorship": { + "message": "Добавить спонсорство" + }, "needsConfirmation": { "message": "Требуется подтверждение" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 2acc04e756d..0fccb850994 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 0c3c4eaba26..8c652c0b815 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -3324,10 +3324,10 @@ "message": "Externé Id sa môže použiť na previazanie tohto zdroja s externým systémom - napríklad s užívateľským adresárom." }, "ssoExternalId": { - "message": "SSO External ID" + "message": "Externé ID SSO" }, "ssoExternalIdDesc": { - "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + "message": "Externé ID SSO je nešifrovaný odkaz medzi Bitwardenom a nakonfigurovaným poskytovateľom SSO." }, "nestCollectionUnder": { "message": "Zaradiť zbierku pod" @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Aktualizovať prehliadač" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Generuje sa váš prehľad o rizikách..." }, "updateBrowserDesc": { @@ -5692,7 +5692,7 @@ "message": "WebAuthn bol úspešne overený! Túto kartu môžete zavrieť." }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "Nové heslo nemôže byť rovnaké ako súčasné heslo." }, "hintEqualsPassword": { "message": "Nápoveda pre heslo nemôže byť rovnaká ako heslo." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden pre Rodiny zadarmo" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Vy a vaša rodina máte bezplatne k dispozícii Bitwarden pre Rodiny. Uplatnite si túto možnosť s vaším osobným emailom aby ste mali dáta v bezpečí aj keď nie ste v práci." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Zdieľané zbierky pre Rodinné heslá" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Pozvať člena" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Potrebné potvrdenie" }, @@ -10593,12 +10623,12 @@ "message": "Bezplatné organizácie môžu mat maximálne dve zbierky. Ak chcete pridať viac zbierok povýšte na platené predplatné." }, "businessUnit": { - "message": "Business Unit" + "message": "Organizačná jednotka" }, "businessUnits": { - "message": "Business Units" + "message": "Organizačné jednotky" }, "newBusinessUnit": { - "message": "New business unit" + "message": "Nová organizačná jednotka" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index fd990234b86..1211a14c630 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 1a414d913a7..bff8820dc2f 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Ажурирајте Претраживач" }, - "generatingRiskInsights": { - "message": "Генерисање прегледа вашег ризика..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Користите неподржани веб прегледач. Веб сеф можда неће правилно функционисати." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Бесплатно Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Ви и ваша породица испуњавате услове за бесплатне Bitwarden Families. Искористите својом личном е-поштом да бисте заштитили своје податке чак и када нисте на послу." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Веза више није важећа. Питајте спонзора да поново пошаље понуду." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Позови Члан" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Потребна је потврда" }, diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 93f2c89a16d..7135f4d90ec 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 5005b6180c7..a81f6398e7a 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Uppdatera webbläsare" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Du använder en webbläsare som inte stöds. Webbvalvet kanske inte fungerar som det ska." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Länken är inte längre giltig. Be sponsorn att skicka erbjudandet igen." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Bjud in medlem" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Kräver bekräftelse" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 293b3b0486f..6a96c3529e2 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 7ea003470cb..1208d27b044 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index da4b35b403e..4d1f7588159 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -4072,7 +4072,7 @@ "updateBrowser": { "message": "Tarayıcıyı güncelle" }, - "generatingRiskInsights": { + "generatingYourRiskInsights": { "message": "Risk içgörüleriniz oluşturuluyor..." }, "updateBrowserDesc": { @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Ücretsiz Bitwarden Aile" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Siz ve aileniz Ücretsiz Bitwarden Aileleri için uygunsunuz. Verilerinizi işte olmadığınızda bile güvende tutmak için kişisel e-postanızla kullanın." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Aile sırları için paylaşılan koleksiyonlar" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Bağlantı artık geçerli değil. Lütfen sponsorun yeniden teklif göndermesini isteyin." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Üye davet et" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Onay gerekli" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index a6bb37ed1d0..0dc579e993d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Оновити браузер" }, - "generatingRiskInsights": { - "message": "Генерується інформація щодо ризиків..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "Ви використовуєте непідтримуваний браузер. Вебсховище може працювати неправильно." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Bitwarden Families безплатно" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "Ви та ваша сім'я маєте право на Bitwarden Families безплатно. Активуйте доступ з особистою електронною адресою, щоб зберігати свої дані захищеними навіть коли ви не на роботі." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Спільні збірки для обміну паролями" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "Посилання більше не дійсне. Попросіть спонсора повторно надіслати пропозицію." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Запросити учасника" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Потребує підтвердження" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 0f2e31d834a..4da814e80ad 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index b3196721a55..f3cf97f8d8c 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "更新浏览器" }, - "generatingRiskInsights": { - "message": "正在生成风险洞察..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "您使用的是不受支持的网页浏览器。网页密码库可能无法正常运行。" @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "免费 Bitwarden 家庭" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "您和您的家人有资格获得免费的 Bitwarden 家庭版计划。使用您的个人电子邮箱兑换,即使您不在工作中,也能确保您的数据安全。" }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "用于家庭机密的共享集合" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "链接已失效。请让赞助方重新发送邀请。" }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "邀请成员" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "需要确认" }, diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 23b932cd52a..e9ad25abf34 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -4072,8 +4072,8 @@ "updateBrowser": { "message": "更新瀏覽器" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "未支援您使用的瀏覽器。網頁版密碼庫可能無法正常運作。" @@ -6305,6 +6305,21 @@ "sponsoredFamilies": { "message": "免費的 Bitwarden 家庭方案" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "您與家庭成員可使用免費的 Bitwarden 家庭方案。就算不在上班時間,也可以使用您的私人電子郵件來兌換此方案,以保障您的資料安全。" }, @@ -6320,6 +6335,18 @@ "sponsoredFamiliesSharedCollections": { "message": "用於家庭機密的共用分類" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "連結已失效。請讓贊助者重新傳送邀請。" }, @@ -7983,6 +8010,9 @@ "inviteMember": { "message": "邀請成員" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "待確認" }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index d3482ea67a5..844c6b779a9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -36,7 +36,7 @@ export class WebProviderService { const orgKey = await this.keyService.getOrgKey(organizationId); const providerKey = await this.keyService.getProviderKey(providerId); - const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey); + const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey); const request = new ProviderAddOrganizationRequest(); request.organizationId = organizationId; @@ -55,7 +55,7 @@ export class WebProviderService { ), ); const providerKey = await this.keyService.getProviderKey(providerId); - const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey); + const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey); await this.providerApiService.addOrganizationToProvider(providerId, { key: encryptedOrgKey.encryptedString, organizationId, @@ -81,8 +81,8 @@ export class WebProviderService { const providerKey = await this.keyService.getProviderKey(providerId); - const encryptedProviderKey = await this.encryptService.encrypt( - organizationKey.key, + const encryptedProviderKey = await this.encryptService.wrapSymmetricKey( + organizationKey, providerKey, ); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index ee0756355cf..230be90b7a4 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -178,7 +178,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); newKeyPair = [ existingUserPublicKeyB64, - await this.encryptService.encrypt(existingUserPrivateKey, userKey[0]), + await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]), ]; } else { newKeyPair = await this.keyService.makeKeyPair(userKey[0]); diff --git a/libs/angular/src/platform/abstractions/view-cache.service.ts b/libs/angular/src/platform/abstractions/view-cache.service.ts index a282ef67967..c5ae6c77d1f 100644 --- a/libs/angular/src/platform/abstractions/view-cache.service.ts +++ b/libs/angular/src/platform/abstractions/view-cache.service.ts @@ -18,6 +18,11 @@ type BaseCacheOptions = { /** An optional injector. Required if the method is called outside of an injection context. */ injector?: Injector; + + /** + * Optional flag to persist the cached value between navigation events. + */ + persistNavigation?: boolean; } & (T extends JsonValue ? Deserializer : Required>); export type SignalCacheOptions = BaseCacheOptions & { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 8e2b3409593..1cc2b591412 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -270,6 +270,10 @@ import { } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { + DefaultEndUserNotificationService, + EndUserNotificationService, +} from "@bitwarden/common/vault/notifications"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, @@ -306,12 +310,7 @@ import { UserAsymmetricKeysRegenerationService, } from "@bitwarden/key-management"; import { SafeInjectionToken } from "@bitwarden/ui-common"; -import { - DefaultEndUserNotificationService, - EndUserNotificationService, - NewDeviceVerificationNoticeService, - PasswordRepromptService, -} from "@bitwarden/vault"; +import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -1489,7 +1488,13 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EndUserNotificationService, useClass: DefaultEndUserNotificationService, - deps: [StateProvider, ApiServiceAbstraction, NotificationsService], + deps: [ + StateProvider, + ApiServiceAbstraction, + NotificationsService, + AuthServiceAbstraction, + LogService, + ], }), safeProvider({ provide: DeviceTrustToastServiceAbstraction, diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts b/libs/angular/src/services/view-password-history.service.spec.ts similarity index 69% rename from apps/web/src/app/vault/services/web-view-password-history.service.spec.ts rename to libs/angular/src/services/view-password-history.service.spec.ts index a4f73ed1a2e..dec2b25b190 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts +++ b/libs/angular/src/services/view-password-history.service.spec.ts @@ -3,17 +3,16 @@ import { TestBed } from "@angular/core/testing"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +import { openPasswordHistoryDialog } from "@bitwarden/vault"; -import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; +import { VaultViewPasswordHistoryService } from "./view-password-history.service"; -import { WebViewPasswordHistoryService } from "./web-view-password-history.service"; - -jest.mock("../individual-vault/password-history.component", () => ({ +jest.mock("@bitwarden/vault", () => ({ openPasswordHistoryDialog: jest.fn(), })); -describe("WebViewPasswordHistoryService", () => { - let service: WebViewPasswordHistoryService; +describe("VaultViewPasswordHistoryService", () => { + let service: VaultViewPasswordHistoryService; let dialogService: DialogService; beforeEach(async () => { @@ -23,13 +22,13 @@ describe("WebViewPasswordHistoryService", () => { await TestBed.configureTestingModule({ providers: [ - WebViewPasswordHistoryService, + VaultViewPasswordHistoryService, { provide: DialogService, useValue: mockDialogService }, Overlay, ], }).compileComponents(); - service = TestBed.inject(WebViewPasswordHistoryService); + service = TestBed.inject(VaultViewPasswordHistoryService); dialogService = TestBed.inject(DialogService); }); diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.ts b/libs/angular/src/services/view-password-history.service.ts similarity index 78% rename from apps/web/src/app/vault/services/web-view-password-history.service.ts rename to libs/angular/src/services/view-password-history.service.ts index b1451b268de..88ca4d37287 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.ts +++ b/libs/angular/src/services/view-password-history.service.ts @@ -3,14 +3,13 @@ import { Injectable } from "@angular/core"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; - -import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; +import { openPasswordHistoryDialog } from "@bitwarden/vault"; /** - * This service is used to display the password history dialog in the web vault. + * This service is used to display the password history dialog in the vault. */ @Injectable() -export class WebViewPasswordHistoryService implements ViewPasswordHistoryService { +export class VaultViewPasswordHistoryService implements ViewPasswordHistoryService { constructor(private dialogService: DialogService) {} /** diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 852302cc0c4..c34816994be 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -18,6 +18,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Directive() @@ -25,13 +26,14 @@ export class VaultItemsComponent implements OnInit, OnDestroy { @Input() activeCipherId: string = null; @Output() onCipherClicked = new EventEmitter(); @Output() onCipherRightClicked = new EventEmitter(); - @Output() onAddCipher = new EventEmitter(); + @Output() onAddCipher = new EventEmitter(); @Output() onAddCipherOptions = new EventEmitter(); loaded = false; ciphers: CipherView[] = []; deleted = false; organization: Organization; + CipherType = CipherType; protected searchPending = false; @@ -109,8 +111,8 @@ export class VaultItemsComponent implements OnInit, OnDestroy { this.onCipherRightClicked.emit(cipher); } - addCipher() { - this.onAddCipher.emit(); + addCipher(type?: CipherType) { + this.onAddCipher.emit(type); } addCipherOptions() { diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 6cdf42b76da..aed9f9f07a5 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -362,7 +362,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.WebAuthn: this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ - pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingIn"), + pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"), pageIcon: TwoFactorAuthWebAuthnIcon, }); break; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index b5846fcfdbf..f7bf2260a36 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -106,7 +106,9 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); + masterPasswordService.masterKeySubject.next( + new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, + ); masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); await sut.approveOrDenyAuthRequest( @@ -115,7 +117,7 @@ describe("AuthRequestService", () => { ); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( - { encKey: new Uint8Array(64) }, + new SymmetricCryptoKey(new Uint8Array(32)), expect.anything(), ); }); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index f4316c2e519..226403d9c8c 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -14,6 +14,7 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, StateProvider, @@ -120,7 +121,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { keyToEncrypt = await this.keyService.getUserKey(); } - const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned( + keyToEncrypt as SymmetricCryptoKey, + pubKey, + ); const response = new PasswordlessAuthRequest( encryptedKey.encryptedString, diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 31f0c5b0177..c0034020de8 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -174,7 +174,8 @@ export class PinService implements PinServiceAbstraction { ); const kdfConfig = await this.kdfConfigService.getKdfConfig(); const pinKey = await this.makePinKey(pin, email, kdfConfig); - return await this.encryptService.encrypt(userKey.key, pinKey); + + return await this.encryptService.wrapSymmetricKey(userKey, pinKey); } async storePinKeyEncryptedUserKey( diff --git a/libs/auth/src/common/services/pin/pin.service.spec.ts b/libs/auth/src/common/services/pin/pin.service.spec.ts index e0b18c74bde..fd33f5d2077 100644 --- a/libs/auth/src/common/services/pin/pin.service.spec.ts +++ b/libs/auth/src/common/services/pin/pin.service.spec.ts @@ -170,7 +170,7 @@ describe("PinService", () => { await sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, mockUserId); // Assert - expect(encryptService.encrypt).toHaveBeenCalledWith(mockUserKey.key, mockPinKey); + expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockUserKey, mockPinKey); }); }); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 1d0b1521db6..e353d79988f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -35,6 +35,10 @@ export enum FeatureFlag { PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", + /* Data Insights and Reporting */ + CriticalApps = "pm-14466-risk-insights-critical-application", + EnableRiskInsightsNotifications = "enable-risk-insights-notifications", + /* Key Management */ PrivateKeyRegeneration = "pm-12241-private-key-regeneration", UserKeyRotationV2 = "userkey-rotation-v2", @@ -43,8 +47,6 @@ export enum FeatureFlag { /* Tools */ ItemShare = "item-share", - CriticalApps = "pm-14466-risk-insights-critical-application", - EnableRiskInsightsNotifications = "enable-risk-insights-notifications", DesktopSendUIRefresh = "desktop-send-ui-refresh", /* Vault */ @@ -55,6 +57,8 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", + EndUserNotifications = "pm-10609-end-user-notifications", /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -91,10 +95,12 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, - /* Tools */ - [FeatureFlag.ItemShare]: FALSE, + /* Data Insights and Reporting */ [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, + + /* Tools */ + [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, /* Vault */ @@ -105,6 +111,8 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, + [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, + [FeatureFlag.EndUserNotifications]: FALSE, /* Auth */ [FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE, diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index a1e54f7064f..5f21d86bc6a 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -7,8 +7,50 @@ import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; export abstract class EncryptService { + /** + * Encrypts a string or Uint8Array to an EncString + * @param plainValue - The value to encrypt + * @param key - The key to encrypt the value with + */ abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; + /** + * Encrypts a value to a Uint8Array + * @param plainValue - The value to encrypt + * @param key - The key to encrypt the value with + */ abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; + + /** + * Wraps a decapsulation key (Private key) with a symmetric key + * @see {@link https://en.wikipedia.org/wiki/Key_wrap} + * @param decapsulationKeyPcks8 - The private key in PKCS8 format + * @param wrappingKey - The symmetric key to wrap the private key with + */ + abstract wrapDecapsulationKey( + decapsulationKeyPcks8: Uint8Array, + wrappingKey: SymmetricCryptoKey, + ): Promise; + /** + * Wraps an encapsulation key (Public key) with a symmetric key + * @see {@link https://en.wikipedia.org/wiki/Key_wrap} + * @param encapsulationKeySpki - The public key in SPKI format + * @param wrappingKey - The symmetric key to wrap the public key with + */ + abstract wrapEncapsulationKey( + encapsulationKeySpki: Uint8Array, + wrappingKey: SymmetricCryptoKey, + ): Promise; + /** + * Wraps a symmetric key with another symmetric key + * @see {@link https://en.wikipedia.org/wiki/Key_wrap} + * @param keyToBeWrapped - The symmetric key to wrap + * @param wrappingKey - The symmetric key to wrap the encapsulated key with + */ + abstract wrapSymmetricKey( + keyToBeWrapped: SymmetricCryptoKey, + wrappingKey: SymmetricCryptoKey, + ): Promise; + /** * Decrypts an EncString to a string * @param encString - The EncString to decrypt @@ -39,6 +81,7 @@ export abstract class EncryptService { /** * Encapsulates a symmetric key with an asymmetric public key * Note: This does not establish sender authenticity + * @see {@link https://en.wikipedia.org/wiki/Key_encapsulation_mechanism} * @param sharedKey - The symmetric key that is to be shared * @param encapsulationKey - The encapsulation key (public key) of the receiver that the key is shared with */ @@ -49,6 +92,7 @@ export abstract class EncryptService { /** * Decapsulates a shared symmetric key with an asymmetric private key * Note: This does not establish sender authenticity + * @see {@link https://en.wikipedia.org/wiki/Key_encapsulation_mechanism} * @param encryptedSharedKey - The encrypted shared symmetric key * @param decapsulationKey - The key to decapsulate with (private key) */ @@ -57,13 +101,13 @@ export abstract class EncryptService { decapsulationKey: Uint8Array, ): Promise; /** - * @deprecated Use encapsulateKeyUnsigned instead + * @deprecated Use @see {@link encapsulateKeyUnsigned} instead * @param data - The data to encrypt * @param publicKey - The public key to encrypt with */ abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; /** - * @deprecated Use decapsulateKeyUnsigned instead + * @deprecated Use @see {@link decapsulateKeyUnsigned} instead * @param data - The ciphertext to decrypt * @param privateKey - The privateKey to decrypt with */ diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 4b299c9c6e6..fceef34421c 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -47,7 +47,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { throw new Error("Type 0 encryption is not supported."); } } @@ -56,22 +56,85 @@ export class EncryptServiceImplementation implements EncryptService { return Promise.resolve(null); } - let plainBuf: Uint8Array; if (typeof plainValue === "string") { - plainBuf = Utils.fromUtf8ToArray(plainValue); + return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key); } else { - plainBuf = plainValue; + return this.encryptUint8Array(plainValue, key); + } + } + + async wrapDecapsulationKey( + decapsulationKeyPkcs8: Uint8Array, + wrappingKey: SymmetricCryptoKey, + ): Promise { + if (decapsulationKeyPkcs8 == null) { + throw new Error("No decapsulation key provided for wrapping."); + } + + if (wrappingKey == null) { + throw new Error("No wrappingKey provided for wrapping."); + } + + return await this.encryptUint8Array(decapsulationKeyPkcs8, wrappingKey); + } + + async wrapEncapsulationKey( + encapsulationKeySpki: Uint8Array, + wrappingKey: SymmetricCryptoKey, + ): Promise { + if (encapsulationKeySpki == null) { + throw new Error("No encapsulation key provided for wrapping."); + } + + if (wrappingKey == null) { + throw new Error("No wrappingKey provided for wrapping."); + } + + return await this.encryptUint8Array(encapsulationKeySpki, wrappingKey); + } + + async wrapSymmetricKey( + keyToBeWrapped: SymmetricCryptoKey, + wrappingKey: SymmetricCryptoKey, + ): Promise { + if (keyToBeWrapped == null) { + throw new Error("No keyToBeWrapped provided for wrapping."); + } + + if (wrappingKey == null) { + throw new Error("No wrappingKey provided for wrapping."); + } + + return await this.encryptUint8Array(keyToBeWrapped.key, wrappingKey); + } + + private async encryptUint8Array( + plainValue: Uint8Array, + key: SymmetricCryptoKey, + ): Promise { + if (key == null) { + throw new Error("No encryption key provided."); + } + + if (this.blockType0) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + + if (plainValue == null) { + return Promise.resolve(null); } const innerKey = key.inner(); if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { - const encObj = await this.aesEncrypt(plainBuf, innerKey); + const encObj = await this.aesEncrypt(plainValue, innerKey); const iv = Utils.fromBufferToB64(encObj.iv); const data = Utils.fromBufferToB64(encObj.data); const mac = Utils.fromBufferToB64(encObj.mac); return new EncString(innerKey.type, data, iv, mac); } else if (innerKey.type === EncryptionType.AesCbc256_B64) { - const encObj = await this.aesEncryptLegacy(plainBuf, innerKey); + const encObj = await this.aesEncryptLegacy(plainValue, innerKey); const iv = Utils.fromBufferToB64(encObj.iv); const data = Utils.fromBufferToB64(encObj.data); return new EncString(innerKey.type, data, iv); @@ -84,7 +147,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { throw new Error("Type 0 encryption is not supported."); } } @@ -124,7 +187,7 @@ export class EncryptServiceImplementation implements EncryptService { if (encString.encryptionType !== innerKey.type) { this.logDecryptError( "Key encryption type does not match payload encryption type", - key.encType, + innerKey.type, encString.encryptionType, decryptContext, ); @@ -148,7 +211,7 @@ export class EncryptServiceImplementation implements EncryptService { if (!macsEqual) { this.logMacFailed( "decryptToUtf8 MAC comparison failed. Key or payload has changed.", - key.encType, + innerKey.type, encString.encryptionType, decryptContext, ); @@ -191,7 +254,7 @@ export class EncryptServiceImplementation implements EncryptService { if (encThing.encryptionType !== inner.type) { this.logDecryptError( "Encryption key type mismatch", - key.encType, + inner.type, encThing.encryptionType, decryptContext, ); @@ -200,19 +263,23 @@ export class EncryptServiceImplementation implements EncryptService { if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) { if (encThing.macBytes == null) { - this.logDecryptError("Mac missing", key.encType, encThing.encryptionType, decryptContext); + this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext); return null; } const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength); macData.set(new Uint8Array(encThing.ivBytes), 0); macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength); - const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); + const computedMac = await this.cryptoFunctionService.hmac( + macData, + inner.authenticationKey, + "sha256", + ); const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { this.logMacFailed( "MAC comparison failed. Key or payload has changed.", - key.encType, + inner.type, encThing.encryptionType, decryptContext, ); @@ -222,14 +289,14 @@ export class EncryptServiceImplementation implements EncryptService { return await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey, + inner.encryptionKey, "cbc", ); } else if (inner.type === EncryptionType.AesCbc256_B64) { return await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey, + inner.encryptionKey, "cbc", ); } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 4cbe3a3da90..bc945a5eff7 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -6,7 +6,10 @@ import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + Aes256CbcHmacKey, + SymmetricCryptoKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { makeStaticByteArray } from "../../../../spec"; @@ -28,6 +31,127 @@ describe("EncryptService", () => { encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true); }); + describe("wrapSymmetricKey", () => { + it("roundtrip encrypts and decrypts a symmetric key", async () => { + cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0)); + cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray); + cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32)); + + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = await encryptService.wrapSymmetricKey(key, wrappingKey); + expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64); + expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0))); + }); + it("fails if key toBeWrapped is null", async () => { + const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + await expect(encryptService.wrapSymmetricKey(null, wrappingKey)).rejects.toThrow( + "No keyToBeWrapped provided for wrapping.", + ); + }); + it("fails if wrapping key is null", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + await expect(encryptService.wrapSymmetricKey(key, null)).rejects.toThrow( + "No wrappingKey provided for wrapping.", + ); + }); + it("fails if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + }); + + describe("wrapDecapsulationKey", () => { + it("roundtrip encrypts and decrypts a decapsulation key", async () => { + cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0)); + cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray); + cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32)); + + const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = await encryptService.wrapDecapsulationKey( + makeStaticByteArray(64), + wrappingKey, + ); + expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64); + expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0))); + }); + it("fails if decapsulation key is null", async () => { + const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + await expect(encryptService.wrapDecapsulationKey(null, wrappingKey)).rejects.toThrow( + "No decapsulation key provided for wrapping.", + ); + }); + it("fails if wrapping key is null", async () => { + const decapsulationKey = makeStaticByteArray(64); + await expect(encryptService.wrapDecapsulationKey(decapsulationKey, null)).rejects.toThrow( + "No wrappingKey provided for wrapping.", + ); + }); + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect( + encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key), + ).rejects.toThrow("Type 0 encryption is not supported."); + }); + }); + + describe("wrapEncapsulationKey", () => { + it("roundtrip encrypts and decrypts an encapsulationKey key", async () => { + cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0)); + cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray); + cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32)); + + const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = await encryptService.wrapEncapsulationKey( + makeStaticByteArray(64), + wrappingKey, + ); + expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64); + expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0))); + }); + it("fails if encapsulation key is null", async () => { + const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + await expect(encryptService.wrapEncapsulationKey(null, wrappingKey)).rejects.toThrow( + "No encapsulation key provided for wrapping.", + ); + }); + it("fails if wrapping key is null", async () => { + const encapsulationKey = makeStaticByteArray(64); + await expect(encryptService.wrapEncapsulationKey(encapsulationKey, null)).rejects.toThrow( + "No wrappingKey provided for wrapping.", + ); + }); + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect( + encryptService.wrapEncapsulationKey(new Uint8Array(200), mock32Key), + ).rejects.toThrow("Type 0 encryption is not supported."); + }); + }); + describe("onServerConfigChange", () => { const newConfig = mock(); @@ -64,6 +188,10 @@ describe("EncryptService", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); await expect(encryptService.encrypt(null!, key)).rejects.toThrow( "Type 0 encryption is not supported.", @@ -146,6 +274,10 @@ describe("EncryptService", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( "Type 0 encryption is not supported.", @@ -228,7 +360,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey), + expect.toEqualBuffer(key.inner().encryptionKey), "cbc", ); @@ -249,7 +381,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey), + expect.toEqualBuffer(key.inner().encryptionKey), "cbc", ); @@ -267,7 +399,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.hmac).toBeCalledWith( expect.toEqualBuffer(expectedMacData), - key.macKey, + (key.inner() as Aes256CbcHmacKey).authenticationKey, "sha256", ); @@ -450,6 +582,12 @@ describe("EncryptService", () => { expect(actual).toEqual(encString); expect(actual.dataBytes).toEqualBuffer(encryptedData); }); + + it("throws if no data was provided", () => { + return expect(encryptService.rsaEncrypt(null, new Uint8Array(32))).rejects.toThrow( + "No data provided for encryption", + ); + }); }); describe("decapsulateKeyUnsigned", () => { diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts index 0c80d508b2d..430774ca2ed 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts @@ -1,6 +1,7 @@ import * as argon2 from "argon2-browser"; import * as forge from "node-forge"; +import { EncryptionType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { CbcDecryptParameters, @@ -247,37 +248,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService { mac: string | null, key: SymmetricCryptoKey, ): CbcDecryptParameters { - const p = {} as CbcDecryptParameters; - if (key.meta != null) { - p.encKey = key.meta.encKeyByteString; - p.macKey = key.meta.macKeyByteString; + const innerKey = key.inner(); + if (innerKey.type === EncryptionType.AesCbc256_B64) { + return { + iv: forge.util.decode64(iv), + data: forge.util.decode64(data), + encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(), + } as CbcDecryptParameters; + } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + const macData = forge.util.decode64(iv) + forge.util.decode64(data); + return { + iv: forge.util.decode64(iv), + data: forge.util.decode64(data), + encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(), + macKey: forge.util.createBuffer(innerKey.authenticationKey).getBytes(), + mac: forge.util.decode64(mac!), + macData, + } as CbcDecryptParameters; + } else { + throw new Error("Unsupported encryption type."); } - - if (p.encKey == null) { - p.encKey = forge.util.decode64(key.encKeyB64); - } - p.data = forge.util.decode64(data); - p.iv = forge.util.decode64(iv); - p.macData = p.iv + p.data; - if (p.macKey == null && key.macKeyB64 != null) { - p.macKey = forge.util.decode64(key.macKeyB64); - } - if (mac != null) { - p.mac = forge.util.decode64(mac); - } - - // cache byte string keys for later - if (key.meta == null) { - key.meta = {}; - } - if (key.meta.encKeyByteString == null) { - key.meta.encKeyByteString = p.encKey; - } - if (p.macKey != null && key.meta.macKeyByteString == null) { - key.meta.macKeyByteString = p.macKey; - } - - return p; } aesDecryptFast({ diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index c82efa0c571..205f332d0f9 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -164,10 +164,10 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { this.encryptService.encapsulateKeyUnsigned(userKey, devicePublicKey), // Encrypt devicePublicKey with user key - this.encryptService.encrypt(devicePublicKey, userKey), + this.encryptService.wrapEncapsulationKey(devicePublicKey, userKey), // Encrypt devicePrivateKey with deviceKey - this.encryptService.encrypt(devicePrivateKey, deviceKey), + this.encryptService.wrapDecapsulationKey(devicePrivateKey, deviceKey), ]); // Send encrypted keys to server @@ -290,7 +290,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); // Re-encrypt the device public key with the new user key - const encryptedDevicePublicKey = await this.encryptService.encrypt( + const encryptedDevicePublicKey = await this.encryptService.wrapEncapsulationKey( decryptedDevicePublicKey, newUserKey, ); diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts index 8431fe4cc35..e78fc01b694 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts @@ -346,8 +346,6 @@ describe("deviceTrustService", () => { const deviceRsaKeyLength = 2048; let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array]; - let mockDevicePrivateKey: Uint8Array; - let mockDevicePublicKey: Uint8Array; let mockDevicePublicKeyEncryptedUserKey: EncString; let mockUserKeyEncryptedDevicePublicKey: EncString; let mockDeviceKeyEncryptedDevicePrivateKey: EncString; @@ -366,7 +364,8 @@ describe("deviceTrustService", () => { let rsaGenerateKeyPairSpy: jest.SpyInstance; let cryptoSvcGetUserKeySpy: jest.SpyInstance; let cryptoSvcRsaEncryptSpy: jest.SpyInstance; - let encryptServiceEncryptSpy: jest.SpyInstance; + let encryptServiceWrapDecapsulationKeySpy: jest.SpyInstance; + let encryptServiceWrapEncapsulationKeySpy: jest.SpyInstance; let appIdServiceGetAppIdSpy: jest.SpyInstance; let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance; @@ -384,9 +383,6 @@ describe("deviceTrustService", () => { new Uint8Array(deviceRsaKeyLength), ]; - mockDevicePublicKey = mockDeviceRsaKeyPair[0]; - mockDevicePrivateKey = mockDeviceRsaKeyPair[1]; - mockDevicePublicKeyEncryptedUserKey = new EncString( EncryptionType.Rsa2048_OaepSha1_B64, "mockDevicePublicKeyEncryptedUserKey", @@ -419,13 +415,17 @@ describe("deviceTrustService", () => { .spyOn(encryptService, "encapsulateKeyUnsigned") .mockResolvedValue(mockDevicePublicKeyEncryptedUserKey); - encryptServiceEncryptSpy = jest - .spyOn(encryptService, "encrypt") + encryptServiceWrapEncapsulationKeySpy = jest + .spyOn(encryptService, "wrapEncapsulationKey") .mockImplementation((plainValue, key) => { - if (plainValue === mockDevicePublicKey && key === mockUserKey) { + if (plainValue instanceof Uint8Array && key instanceof SymmetricCryptoKey) { return Promise.resolve(mockUserKeyEncryptedDevicePublicKey); } - if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) { + }); + encryptServiceWrapDecapsulationKeySpy = jest + .spyOn(encryptService, "wrapDecapsulationKey") + .mockImplementation((plainValue, key) => { + if (plainValue instanceof Uint8Array && key instanceof SymmetricCryptoKey) { return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey); } }); @@ -452,7 +452,8 @@ describe("deviceTrustService", () => { const userKey = cryptoSvcRsaEncryptSpy.mock.calls[0][0]; expect(userKey.key.byteLength).toBe(64); - expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2); + expect(encryptServiceWrapDecapsulationKeySpy).toHaveBeenCalledTimes(1); + expect(encryptServiceWrapEncapsulationKeySpy).toHaveBeenCalledTimes(1); expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1); expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1); @@ -508,9 +509,14 @@ describe("deviceTrustService", () => { errorText: "rsaEncrypt error", }, { - method: "encryptService.encrypt", - spy: () => encryptServiceEncryptSpy, - errorText: "encryptService.encrypt error", + method: "encryptService.wrapEncapsulationKey", + spy: () => encryptServiceWrapEncapsulationKeySpy, + errorText: "encryptService.wrapEncapsulationKey error", + }, + { + method: "encryptService.wrapDecapsulationKey", + spy: () => encryptServiceWrapDecapsulationKeySpy, + errorText: "encryptService.wrapDecapsulationKey error", }, ]; @@ -872,7 +878,7 @@ describe("deviceTrustService", () => { }); // Mock the reencryption of the device public key with the new user key - encryptService.encrypt.mockImplementationOnce((plainValue, key) => { + encryptService.wrapEncapsulationKey.mockImplementationOnce((plainValue, key) => { expect(plainValue).toBeInstanceOf(Uint8Array); expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker); diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts index fd3ce0c4777..b88ada56129 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts @@ -252,7 +252,9 @@ describe("KeyConnectorService", () => { const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); masterPasswordService.masterKeySubject.next(masterKey); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); @@ -273,7 +275,9 @@ describe("KeyConnectorService", () => { // Arrange const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); const error = new Error("Failed to post user key to key connector"); organizationService.organizations$.mockReturnValue(of([organization])); diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index 91b8e9100ac..9799f06f64a 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -95,7 +95,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; const organization = await this.getManagingOrganization(userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); try { await this.apiService.postUserKeyToKeyConnector( @@ -157,7 +159,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.tokenService.getEmail(), kdfConfig, ); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.keyService.makeUserKey(masterKey); diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index aa0ecc97b58..d1bf96b1956 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -1,3 +1,5 @@ +import { NotificationViewResponse as EndUserNotificationResponse } from "@bitwarden/common/vault/notifications/models"; + import { NotificationType } from "../../enums"; import { BaseResponse } from "./base.response"; @@ -57,6 +59,10 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncOrganizationCollectionSettingChanged: this.payload = new OrganizationCollectionSettingChangedPushNotification(payload); break; + case NotificationType.Notification: + case NotificationType.NotificationStatus: + this.payload = new EndUserNotificationResponse(payload); + break; default: break; } diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts index cce99b847bb..6b641ad443a 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts @@ -2,7 +2,7 @@ import { makeStaticByteArray } from "../../../../spec"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; +import { Aes256CbcHmacKey, SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("SymmetricCryptoKey", () => { it("errors if no key", () => { @@ -19,13 +19,8 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - encKey: key, - encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - encType: EncryptionType.AesCbc256_B64, key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - macKey: null, - macKeyB64: undefined, innerKey: { type: EncryptionType.AesCbc256_B64, encryptionKey: key, @@ -38,14 +33,9 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - encKey: key.slice(0, 32), - encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - encType: EncryptionType.AesCbc256_HmacSha256_B64, key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==", - macKey: key.slice(32, 64), - macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=", innerKey: { type: EncryptionType.AesCbc256_HmacSha256_B64, encryptionKey: key.slice(0, 32), @@ -86,8 +76,8 @@ describe("SymmetricCryptoKey", () => { expect(actual).toEqual({ type: EncryptionType.AesCbc256_HmacSha256_B64, - encryptionKey: key.encKey, - authenticationKey: key.macKey, + encryptionKey: key.inner().encryptionKey, + authenticationKey: (key.inner() as Aes256CbcHmacKey).authenticationKey, }); }); @@ -95,7 +85,7 @@ describe("SymmetricCryptoKey", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const actual = key.toEncoded(); - expect(actual).toEqual(key.encKey); + expect(actual).toEqual(key.inner().encryptionKey); }); it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => { diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index 45e15c1f602..c85f3432b28 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -25,15 +25,7 @@ export class SymmetricCryptoKey { private innerKey: Aes256CbcHmacKey | Aes256CbcKey; key: Uint8Array; - encKey: Uint8Array; - macKey?: Uint8Array; - encType: EncryptionType; - keyB64: string; - encKeyB64: string; - macKeyB64: string; - - meta: any; /** * @param key The key in one of the permitted serialization formats @@ -48,30 +40,16 @@ export class SymmetricCryptoKey { type: EncryptionType.AesCbc256_B64, encryptionKey: key, }; - this.encType = EncryptionType.AesCbc256_B64; this.key = key; - this.keyB64 = Utils.fromBufferToB64(this.key); - - this.encKey = key; - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - - this.macKey = null; - this.macKeyB64 = undefined; + this.keyB64 = this.toBase64(); } else if (key.byteLength === 64) { this.innerKey = { type: EncryptionType.AesCbc256_HmacSha256_B64, encryptionKey: key.slice(0, 32), authenticationKey: key.slice(32), }; - this.encType = EncryptionType.AesCbc256_HmacSha256_B64; this.key = key; - this.keyB64 = Utils.fromBufferToB64(this.key); - - this.encKey = key.slice(0, 32); - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - - this.macKey = key.slice(32); - this.macKeyB64 = Utils.fromBufferToB64(this.macKey); + this.keyB64 = this.toBase64(); } else { throw new Error(`Unsupported encType/key length ${key.byteLength}`); } diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 26cc0a46708..cd8d52fe373 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -479,7 +479,7 @@ describe("SendService", () => { beforeEach(() => { encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Send Key"); - encryptService.encrypt.mockResolvedValue(encryptedKey); + encryptService.wrapSymmetricKey.mockResolvedValue(encryptedKey); }); it("returns re-encrypted user sends", async () => { diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 1b5e5f6aa31..cefd9942d29 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -50,7 +50,7 @@ export class SendService implements InternalSendServiceAbstraction { model: SendView, file: File | ArrayBuffer, password: string, - key?: SymmetricCryptoKey, + userKey?: SymmetricCryptoKey, ): Promise<[Send, EncArrayBuffer]> { let fileData: EncArrayBuffer = null; const send = new Send(); @@ -62,15 +62,19 @@ export class SendService implements InternalSendServiceAbstraction { send.deletionDate = model.deletionDate; send.expirationDate = model.expirationDate; if (model.key == null) { + // Sends use a seed, stored in the URL fragment. This seed is used to derive the key that is used for encryption. const key = await this.keyGenerationService.createKeyWithPurpose( 128, this.sendKeyPurpose, this.sendKeySalt, ); + // key.material is the seed that can be used to re-derive the key model.key = key.material; model.cryptoKey = key.derivedKey; } if (password != null) { + // Note: Despite being called key, the passwordKey is not used for encryption. + // It is used as a static proof that the client knows the password, and has the encryption key. const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, @@ -78,10 +82,11 @@ export class SendService implements InternalSendServiceAbstraction { ); send.password = passwordKey.keyB64; } - if (key == null) { - key = await this.keyService.getUserKey(); + if (userKey == null) { + userKey = await this.keyService.getUserKey(); } - send.key = await this.encryptService.encrypt(model.key, key); + // Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey + send.key = await this.encryptService.encrypt(model.key, userKey); send.name = await this.encryptService.encrypt(model.name, model.cryptoKey); send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey); if (send.type === SendType.Text) { @@ -287,8 +292,10 @@ export class SendService implements InternalSendServiceAbstraction { ) { const requests = await Promise.all( sends.map(async (send) => { - const sendKey = await this.encryptService.decryptToBytes(send.key, originalUserKey); - send.key = await this.encryptService.encrypt(sendKey, rotateUserKey); + const sendKey = new SymmetricCryptoKey( + await this.encryptService.decryptToBytes(send.key, originalUserKey), + ); + send.key = await this.encryptService.wrapSymmetricKey(sendKey, rotateUserKey); return new SendWithIdRequest(send); }), ); diff --git a/libs/vault/src/notifications/abstractions/end-user-notification.service.ts b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts similarity index 64% rename from libs/vault/src/notifications/abstractions/end-user-notification.service.ts rename to libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts index fe2852994f7..bc5dd4d97a4 100644 --- a/libs/vault/src/notifications/abstractions/end-user-notification.service.ts +++ b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts @@ -1,6 +1,6 @@ -import { Observable } from "rxjs"; +import { Observable, Subscription } from "rxjs"; -import { UserId } from "@bitwarden/common/types/guid"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; import { NotificationView } from "../models"; @@ -25,18 +25,23 @@ export abstract class EndUserNotificationService { * @param notificationId * @param userId */ - abstract markAsRead(notificationId: any, userId: UserId): Promise; + abstract markAsRead(notificationId: NotificationId, userId: UserId): Promise; /** * Mark a notification as deleted. * @param notificationId * @param userId */ - abstract markAsDeleted(notificationId: any, userId: UserId): Promise; + abstract markAsDeleted(notificationId: NotificationId, userId: UserId): Promise; /** * Clear all notifications from state for the given user. * @param userId */ abstract clearState(userId: UserId): Promise; + + /** + * Creates a subscription to listen for end user push notifications and notification status updates. + */ + abstract listenForEndUserNotifications(): Subscription; } diff --git a/libs/common/src/vault/notifications/index.ts b/libs/common/src/vault/notifications/index.ts new file mode 100644 index 00000000000..768262be943 --- /dev/null +++ b/libs/common/src/vault/notifications/index.ts @@ -0,0 +1,2 @@ +export { EndUserNotificationService } from "./abstractions/end-user-notification.service"; +export { DefaultEndUserNotificationService } from "./services/default-end-user-notification.service"; diff --git a/libs/vault/src/notifications/models/index.ts b/libs/common/src/vault/notifications/models/index.ts similarity index 100% rename from libs/vault/src/notifications/models/index.ts rename to libs/common/src/vault/notifications/models/index.ts diff --git a/libs/vault/src/notifications/models/notification-view.data.ts b/libs/common/src/vault/notifications/models/notification-view.data.ts similarity index 85% rename from libs/vault/src/notifications/models/notification-view.data.ts rename to libs/common/src/vault/notifications/models/notification-view.data.ts index 07c147052ad..60314a44684 100644 --- a/libs/vault/src/notifications/models/notification-view.data.ts +++ b/libs/common/src/vault/notifications/models/notification-view.data.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { NotificationId } from "@bitwarden/common/types/guid"; +import { NotificationId, SecurityTaskId } from "@bitwarden/common/types/guid"; import { NotificationViewResponse } from "./notification-view.response"; @@ -10,6 +10,7 @@ export class NotificationViewData { title: string; body: string; date: Date; + taskId?: SecurityTaskId; readDate: Date | null; deletedDate: Date | null; @@ -19,6 +20,7 @@ export class NotificationViewData { this.title = response.title; this.body = response.body; this.date = response.date; + this.taskId = response.taskId; this.readDate = response.readDate; this.deletedDate = response.deletedDate; } @@ -30,6 +32,7 @@ export class NotificationViewData { title: obj.title, body: obj.body, date: new Date(obj.date), + taskId: obj.taskId, readDate: obj.readDate ? new Date(obj.readDate) : null, deletedDate: obj.deletedDate ? new Date(obj.deletedDate) : null, }); diff --git a/libs/vault/src/notifications/models/notification-view.response.ts b/libs/common/src/vault/notifications/models/notification-view.response.ts similarity index 81% rename from libs/vault/src/notifications/models/notification-view.response.ts rename to libs/common/src/vault/notifications/models/notification-view.response.ts index bbebf25bd4e..b4b7d8d94cc 100644 --- a/libs/vault/src/notifications/models/notification-view.response.ts +++ b/libs/common/src/vault/notifications/models/notification-view.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { NotificationId } from "@bitwarden/common/types/guid"; +import { NotificationId, SecurityTaskId } from "@bitwarden/common/types/guid"; export class NotificationViewResponse extends BaseResponse { id: NotificationId; @@ -7,6 +7,7 @@ export class NotificationViewResponse extends BaseResponse { title: string; body: string; date: Date; + taskId?: SecurityTaskId; readDate: Date; deletedDate: Date; @@ -17,6 +18,7 @@ export class NotificationViewResponse extends BaseResponse { this.title = this.getResponseProperty("Title"); this.body = this.getResponseProperty("Body"); this.date = this.getResponseProperty("Date"); + this.taskId = this.getResponseProperty("TaskId"); this.readDate = this.getResponseProperty("ReadDate"); this.deletedDate = this.getResponseProperty("DeletedDate"); } diff --git a/libs/vault/src/notifications/models/notification-view.ts b/libs/common/src/vault/notifications/models/notification-view.ts similarity index 75% rename from libs/vault/src/notifications/models/notification-view.ts rename to libs/common/src/vault/notifications/models/notification-view.ts index b577a889d05..21d55ec0aed 100644 --- a/libs/vault/src/notifications/models/notification-view.ts +++ b/libs/common/src/vault/notifications/models/notification-view.ts @@ -1,4 +1,4 @@ -import { NotificationId } from "@bitwarden/common/types/guid"; +import { NotificationId, SecurityTaskId } from "@bitwarden/common/types/guid"; export class NotificationView { id: NotificationId; @@ -6,6 +6,7 @@ export class NotificationView { title: string; body: string; date: Date; + taskId?: SecurityTaskId; readDate: Date | null; deletedDate: Date | null; @@ -15,6 +16,7 @@ export class NotificationView { this.title = obj.title; this.body = obj.body; this.date = obj.date; + this.taskId = obj.taskId; this.readDate = obj.readDate; this.deletedDate = obj.deletedDate; } diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts new file mode 100644 index 00000000000..89a78d6f7d2 --- /dev/null +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts @@ -0,0 +1,223 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { NotificationViewResponse } from "../models"; +import { NOTIFICATIONS } from "../state/end-user-notification.state"; + +import { + DEFAULT_NOTIFICATION_PAGE_SIZE, + DefaultEndUserNotificationService, +} from "./default-end-user-notification.service"; + +describe("End User Notification Center Service", () => { + let fakeStateProvider: FakeStateProvider; + let mockApiService: jest.Mocked; + let mockNotificationsService: jest.Mocked; + let mockAuthService: jest.Mocked; + let mockLogService: jest.Mocked; + let service: DefaultEndUserNotificationService; + + beforeEach(() => { + fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); + mockApiService = { + send: jest.fn(), + } as any; + mockNotificationsService = { + notifications$: of(null), + } as any; + mockAuthService = { + authStatuses$: of({}), + } as any; + mockLogService = mock(); + + service = new DefaultEndUserNotificationService( + fakeStateProvider as unknown as StateProvider, + mockApiService, + mockNotificationsService, + mockAuthService, + mockLogService, + ); + }); + + describe("notifications$", () => { + it("should return notifications from state when not null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + } as NotificationViewResponse, + ]); + + const result = await firstValueFrom(service.notifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiService.send).not.toHaveBeenCalled(); + expect(mockLogService.warning).not.toHaveBeenCalled(); + }); + + it("should return notifications API when state is null", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, null as any); + + const result = await firstValueFrom(service.notifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + expect(mockLogService.warning).not.toHaveBeenCalled(); + }); + + it("should log a warning if there are more notifications available", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + ...new Array(DEFAULT_NOTIFICATION_PAGE_SIZE + 1).fill({ id: "notification-id" }), + ] as NotificationViewResponse[], + continuationToken: "next-token", // Presence of continuation token indicates more data + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, null as any); + + const result = await firstValueFrom(service.notifications$("user-id" as UserId)); + + expect(result.length).toBe(DEFAULT_NOTIFICATION_PAGE_SIZE + 1); + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + expect(mockLogService.warning).toHaveBeenCalledWith( + `More notifications available, but not fetched. Consider increasing the page size from ${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + ); + }); + + it("should share the same observable for the same user", async () => { + const first = service.notifications$("user-id" as UserId); + const second = service.notifications$("user-id" as UserId); + + expect(first).toBe(second); + }); + }); + + describe("unreadNotifications$", () => { + it("should return unread notifications from state when read value is null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + readDate: null as any, + } as NotificationViewResponse, + ]); + + const result = await firstValueFrom(service.unreadNotifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiService.send).not.toHaveBeenCalled(); + }); + }); + + describe("getNotifications", () => { + it("should call getNotifications returning notifications from API", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + await service.refreshNotifications("user-id" as UserId); + + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + }); + + it("should update local state when notifications are updated", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + const mock = fakeStateProvider.singleUser.mockFor( + "user-id" as UserId, + NOTIFICATIONS, + null as any, + ); + + await service.refreshNotifications("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([ + expect.objectContaining({ + id: "notification-id" as NotificationId, + } as NotificationViewResponse), + ]); + }); + }); + + describe("clear", () => { + it("should clear the local notification state for the user", async () => { + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + } as NotificationViewResponse, + ]); + + await service.clearState("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([]); + }); + }); + + describe("markAsDeleted", () => { + it("should send an API request to mark the notification as deleted", async () => { + await service.markAsDeleted("notification-id" as NotificationId, "user-id" as UserId); + expect(mockApiService.send).toHaveBeenCalledWith( + "DELETE", + "/notifications/notification-id/delete", + null, + true, + false, + ); + }); + }); + + describe("markAsRead", () => { + it("should send an API request to mark the notification as read", async () => { + await service.markAsRead("notification-id" as NotificationId, "user-id" as UserId); + expect(mockApiService.send).toHaveBeenCalledWith( + "PATCH", + "/notifications/notification-id/read", + null, + true, + false, + ); + }); + }); +}); diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts new file mode 100644 index 00000000000..87f97b48c27 --- /dev/null +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts @@ -0,0 +1,213 @@ +import { concatMap, EMPTY, filter, map, Observable, Subscription, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { NotificationType } from "@bitwarden/common/enums"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; +import { + filterOutNullish, + perUserCache$, +} from "@bitwarden/common/vault/utils/observable-utilities"; + +import { EndUserNotificationService } from "../abstractions/end-user-notification.service"; +import { NotificationView, NotificationViewData, NotificationViewResponse } from "../models"; +import { NOTIFICATIONS } from "../state/end-user-notification.state"; + +/** + * The default number of notifications to fetch from the API. + */ +export const DEFAULT_NOTIFICATION_PAGE_SIZE = 50; + +const getLoggedInUserIds = map, UserId[]>((authStatuses) => + Object.entries(authStatuses ?? {}) + .filter(([, status]) => status >= AuthenticationStatus.Locked) + .map(([userId]) => userId as UserId), +); + +/** + * A service for retrieving and managing notifications for end users. + */ +export class DefaultEndUserNotificationService implements EndUserNotificationService { + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + private notificationService: NotificationsService, + private authService: AuthService, + private logService: LogService, + ) {} + + notifications$ = perUserCache$((userId: UserId): Observable => { + return this.notificationState(userId).state$.pipe( + switchMap(async (notifications) => { + if (notifications == null) { + await this.fetchNotificationsFromApi(userId); + return null; + } + return notifications; + }), + filterOutNullish(), + map((notifications) => + notifications.map((notification) => new NotificationView(notification)), + ), + ); + }); + + unreadNotifications$ = perUserCache$((userId: UserId): Observable => { + return this.notifications$(userId).pipe( + map((notifications) => notifications.filter((notification) => notification.readDate == null)), + ); + }); + + async markAsRead(notificationId: NotificationId, userId: UserId): Promise { + await this.apiService.send("PATCH", `/notifications/${notificationId}/read`, null, true, false); + await this.notificationState(userId).update((current) => { + const notification = current?.find((n) => n.id === notificationId); + if (notification) { + notification.readDate = new Date(); + } + return current; + }); + } + + async markAsDeleted(notificationId: NotificationId, userId: UserId): Promise { + await this.apiService.send( + "DELETE", + `/notifications/${notificationId}/delete`, + null, + true, + false, + ); + await this.notificationState(userId).update((current) => { + const notification = current?.find((n) => n.id === notificationId); + if (notification) { + notification.deletedDate = new Date(); + } + return current; + }); + } + + async clearState(userId: UserId): Promise { + await this.replaceNotificationState(userId, []); + } + + async refreshNotifications(userId: UserId) { + await this.fetchNotificationsFromApi(userId); + } + + /** + * Helper observable to filter notifications by the notification type and user ids + * Returns EMPTY if no user ids are provided + * @param userIds + * @private + */ + private filteredEndUserNotifications$(userIds: UserId[]) { + if (userIds.length == 0) { + return EMPTY; + } + + return this.notificationService.notifications$.pipe( + filter( + ([{ type }, userId]) => + (type === NotificationType.Notification || + type === NotificationType.NotificationStatus) && + userIds.includes(userId), + ), + ); + } + + /** + * Creates a subscription to listen for end user push notifications and notification status updates. + */ + listenForEndUserNotifications(): Subscription { + return this.authService.authStatuses$ + .pipe( + getLoggedInUserIds, + switchMap((userIds) => this.filteredEndUserNotifications$(userIds)), + concatMap(([notification, userId]) => + this.upsertNotification( + userId, + new NotificationViewData(notification.payload as NotificationViewResponse), + ), + ), + ) + .subscribe(); + } + + /** + * Fetches the notifications from the API and updates the local state + * @param userId + * @private + */ + private async fetchNotificationsFromApi(userId: UserId): Promise { + const res = await this.apiService.send( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + const response = new ListResponse(res, NotificationViewResponse); + + if (response.continuationToken != null) { + this.logService.warning( + `More notifications available, but not fetched. Consider increasing the page size from ${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + ); + } + + const notificationData = response.data.map((n) => new NotificationViewData(n)); + await this.replaceNotificationState(userId, notificationData); + } + + /** + * Replaces the local state with notifications and returns the updated state + * @param userId + * @param notifications + * @private + */ + private replaceNotificationState( + userId: UserId, + notifications: NotificationViewData[], + ): Promise { + return this.notificationState(userId).update(() => notifications); + } + + /** + * Updates the local state adding the new notification or updates an existing one with the same id + * Returns the entire updated notifications state + * @param userId + * @param notification + * @private + */ + private async upsertNotification( + userId: UserId, + notification: NotificationViewData, + ): Promise { + return this.notificationState(userId).update((current) => { + current ??= []; + + const existingIndex = current.findIndex((n) => n.id === notification.id); + + if (existingIndex === -1) { + current.push(notification); + } else { + current[existingIndex] = notification; + } + + return current; + }); + } + + /** + * Returns the local state for notifications + * @param userId + * @private + */ + private notificationState(userId: UserId) { + return this.stateProvider.getUser(userId, NOTIFICATIONS); + } +} diff --git a/libs/vault/src/notifications/state/end-user-notification.state.ts b/libs/common/src/vault/notifications/state/end-user-notification.state.ts similarity index 100% rename from libs/vault/src/notifications/state/end-user-notification.state.ts rename to libs/common/src/vault/notifications/state/end-user-notification.state.ts diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 24903e229ca..57df5f2a376 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -280,6 +280,7 @@ describe("Cipher Service", () => { Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey), ); encryptService.encrypt.mockImplementation(encryptText); + encryptService.wrapSymmetricKey.mockResolvedValue(new EncString("Re-encrypted Cipher Key")); jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true); }); @@ -436,7 +437,7 @@ describe("Cipher Service", () => { encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Cipher Key"); - encryptService.encrypt.mockResolvedValue(encryptedKey); + encryptService.wrapSymmetricKey.mockResolvedValue(encryptedKey); keyService.makeCipherKey.mockResolvedValue( new SymmetricCryptoKey(new Uint8Array(32)) as CipherKey, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index c192876c83e..455be3babea 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -266,7 +266,7 @@ export class CipherService implements CipherServiceAbstraction { key, ).then(async () => { if (model.key != null) { - attachment.key = await this.encryptService.encrypt(model.key.key, key); + attachment.key = await this.encryptService.wrapSymmetricKey(model.key, key); } encAttachments.push(attachment); }); @@ -1820,8 +1820,8 @@ export class CipherService implements CipherServiceAbstraction { } // Then, we have to encrypt the cipher key with the proper key. - cipher.key = await this.encryptService.encrypt( - decryptedCipherKey.key, + cipher.key = await this.encryptService.wrapSymmetricKey( + decryptedCipherKey, keyForCipherKeyEncryption, ); diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index 27abccdd45f..46eb1b15b16 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, ElementRef, Input, NgZone, OnInit, Optional } from "@angular/core"; +import { AfterContentChecked, Directive, ElementRef, Input, NgZone, Optional } from "@angular/core"; import { take } from "rxjs/operators"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -12,40 +12,72 @@ import { FocusableElement } from "../shared/focusable-element"; * * @remarks * + * Will focus the element once, when it becomes visible. + * * If the component provides the `FocusableElement` interface, the `focus` * method will be called. Otherwise, the native element will be focused. */ @Directive({ selector: "[appAutofocus], [bitAutofocus]", }) -export class AutofocusDirective implements OnInit { +export class AutofocusDirective implements AfterContentChecked { @Input() set appAutofocus(condition: boolean | string) { this.autofocus = condition === "" || condition === true; } private autofocus: boolean; + // Track if we have already focused the element. + private focused = false; + constructor( private el: ElementRef, private ngZone: NgZone, @Optional() private focusableElement: FocusableElement, ) {} - ngOnInit() { - if (!Utils.isMobileBrowser && this.autofocus) { - if (this.ngZone.isStable) { - this.focus(); - } else { - this.ngZone.onStable.pipe(take(1)).subscribe(this.focus.bind(this)); - } + /** + * Using AfterContentChecked is a hack to ensure we only focus once. This is because + * the element may not be in the DOM, or not be focusable when the directive is + * created, and we want to wait until it is. + * + * Note: This might break in the future since it relies on Angular change detection + * to trigger after the element becomes visible. + */ + ngAfterContentChecked() { + // We only want to focus the element on initial render and it's not a mobile browser + if (this.focused || !this.autofocus || Utils.isMobileBrowser) { + return; + } + + const el = this.getElement(); + if (el == null) { + return; + } + + if (this.ngZone.isStable) { + this.focus(); + } else { + this.ngZone.onStable.pipe(take(1)).subscribe(this.focus.bind(this)); } } + /** + * Attempt to focus the element. If successful we set focused to true to prevent further focus + * attempts. + */ private focus() { + const el = this.getElement(); + + el.focus(); + this.focused = el === document.activeElement; + } + + private getElement() { if (this.focusableElement) { - this.focusableElement.getFocusTarget().focus(); - } else { - this.el.nativeElement.focus(); + return this.focusableElement.getFocusTarget(); } + + return this.el.nativeElement; } } diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index 7f1bd781e9d..7edf3b1d60a 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -49,7 +49,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { @Input() autocomplete: string; getFocusTarget() { - return this.input.nativeElement; + return this.input?.nativeElement; } onChange(searchText: string) { diff --git a/libs/components/src/shared/focusable-element.ts b/libs/components/src/shared/focusable-element.ts index 7b063f4ddc9..99340d5a7bf 100644 --- a/libs/components/src/shared/focusable-element.ts +++ b/libs/components/src/shared/focusable-element.ts @@ -6,5 +6,5 @@ * Used by the `AutofocusDirective` and `A11yGridDirective`. */ export abstract class FocusableElement { - getFocusTarget: () => HTMLElement; + getFocusTarget: () => HTMLElement | undefined; } diff --git a/libs/key-management-ui/src/lock/components/lock.component.html b/libs/key-management-ui/src/lock/components/lock.component.html index 437e29447e2..efc7fb26a2f 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.html +++ b/libs/key-management-ui/src/lock/components/lock.component.html @@ -1,10 +1,10 @@ - -
+ +
- +